diff options
author | crupest <crupest@outlook.com> | 2021-02-15 01:08:05 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-02-15 01:08:05 +0800 |
commit | 58e23e759d730dd9d9733a64e5f16cc5aafeba35 (patch) | |
tree | 53a050eb3b20506eae7825d0b5a063562144366e | |
parent | da0b49f48a8e18fb19281d03b910342e05ffe649 (diff) | |
download | timeline-58e23e759d730dd9d9733a64e5f16cc5aafeba35.tar.gz timeline-58e23e759d730dd9d9733a64e5f16cc5aafeba35.tar.bz2 timeline-58e23e759d730dd9d9733a64e5f16cc5aafeba35.zip |
refactor: Refactor timeline card.
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx | 137 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx | 173 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx | 213 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx | 167 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline/TimelineCard.tsx | 85 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx | 10 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx | 70 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline/TimelinePageUI.tsx | 20 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline/index.tsx | 33 | ||||
-rw-r--r-- | FrontEnd/src/app/views/user/UserCard.tsx | 107 | ||||
-rw-r--r-- | FrontEnd/src/app/views/user/UserInfoCard.tsx | 70 | ||||
-rw-r--r-- | FrontEnd/src/app/views/user/UserPageUI.tsx | 18 | ||||
-rw-r--r-- | FrontEnd/src/app/views/user/index.tsx | 44 |
13 files changed, 516 insertions, 631 deletions
diff --git a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx deleted file mode 100644 index c29e628d..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; -import { Dropdown, Button } from "react-bootstrap"; - -import { getHttpHighlightClient } from "@/http/highlight"; -import { getHttpBookmarkClient } from "@/http/bookmark"; - -import { useUser } from "@/services/user"; -import { pushAlert } from "@/services/alert"; -import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; - -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import CollapseButton from "../timeline-common/CollapseButton"; - -export interface TimelineCardTemplateProps - extends Omit<TimelineCardComponentProps<"">, "operations"> { - infoArea: React.ReactElement; - manageArea: - | { type: "member"; onMember: () => void } - | { - type: "manage"; - items: ( - | { - type: "button"; - text: string; - color?: string; - onClick: () => void; - } - | { type: "divider" } - )[]; - }; -} - -function TimelineCardTemplate({ - timeline, - collapse, - infoArea, - manageArea, - toggleCollapse, - className, -}: TimelineCardTemplateProps): React.ReactElement | null { - const { t } = useTranslation(); - - const user = useUser(); - - return ( - <div className={clsx("cru-card p-2 clearfix", className)}> - <div className="float-right d-flex align-items-center"> - <CollapseButton collapse={collapse} onClick={toggleCollapse} /> - </div> - <div style={{ display: collapse ? "none" : "block" }}> - {infoArea} - <p className="mb-0">{timeline.description}</p> - <small className="mt-1 d-block"> - {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} - </small> - <div className="text-right mt-2"> - <i - className={clsx( - timeline.isHighlight ? "bi-star-fill" : "bi-star", - "icon-button text-yellow mr-3" - )} - onClick={ - user != null && user.hasHighlightTimelineAdministrationPermission - ? () => { - getHttpHighlightClient() - [timeline.isHighlight ? "delete" : "put"](timeline.name) - .catch(() => { - pushAlert({ - message: timeline.isHighlight - ? "timeline.removeHighlightFail" - : "timeline.addHighlightFail", - type: "danger", - }); - }); - } - : undefined - } - /> - {user != null ? ( - <i - className={clsx( - timeline.isBookmark ? "bi-bookmark-fill" : "bi-bookmark", - "icon-button text-yellow mr-3" - )} - onClick={() => { - getHttpBookmarkClient() - [timeline.isBookmark ? "delete" : "put"](timeline.name) - .catch(() => { - pushAlert({ - message: timeline.isBookmark - ? "timeline.removeBookmarkFail" - : "timeline.addBookmarkFail", - type: "danger", - }); - }); - }} - /> - ) : null} - {manageArea.type === "manage" ? ( - <Dropdown className="d-inline-block"> - <Dropdown.Toggle variant="outline-primary"> - {t("timeline.manage")} - </Dropdown.Toggle> - <Dropdown.Menu> - {manageArea.items.map((item, index) => { - if (item.type === "divider") { - return <Dropdown.Divider key={index} />; - } else { - return ( - <Dropdown.Item - key={index} - onClick={item.onClick} - className={ - item.color != null ? "text-" + item.color : undefined - } - > - {t(item.text)} - </Dropdown.Item> - ); - } - })} - </Dropdown.Menu> - </Dropdown> - ) : ( - <Button variant="outline-primary" onClick={manageArea.onMember}> - {t("timeline.memberButton")} - </Button> - )} - </div> - </div> - </div> - ); -} - -export default TimelineCardTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx new file mode 100644 index 00000000..921f1390 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { Dropdown, Button } from "react-bootstrap"; + +import { getHttpHighlightClient } from "@/http/highlight"; +import { getHttpBookmarkClient } from "@/http/bookmark"; + +import { useUser } from "@/services/user"; +import { pushAlert } from "@/services/alert"; +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; + +import { TimelinePageCardProps } from "./TimelinePageTemplate"; + +import CollapseButton from "./CollapseButton"; +import { TimelineMemberDialog } from "./TimelineMember"; +import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; + +export interface TimelineCardTemplateProps extends TimelinePageCardProps { + infoArea: React.ReactElement; + manageArea: + | { type: "member" } + | { + type: "manage"; + items: ( + | { + type: "button"; + text: string; + color?: string; + onClick: () => void; + } + | { type: "divider" } + )[]; + }; + dialog: string | "property" | "member" | null; + setDialog: (dialog: "property" | "member" | null) => void; +} + +const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({ + timeline, + collapse, + toggleCollapse, + infoArea, + manageArea, + onReload, + className, + dialog, + setDialog, +}) => { + const { t } = useTranslation(); + + const user = useUser(); + + return ( + <> + <div className={clsx("cru-card p-2 clearfix", className)}> + <div className="float-right d-flex align-items-center"> + <CollapseButton collapse={collapse} onClick={toggleCollapse} /> + </div> + <div style={{ display: collapse ? "none" : "block" }}> + {infoArea} + <p className="mb-0">{timeline.description}</p> + <small className="mt-1 d-block"> + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + </small> + <div className="text-right mt-2"> + <i + className={clsx( + timeline.isHighlight ? "bi-star-fill" : "bi-star", + "icon-button text-yellow mr-3" + )} + onClick={ + user != null && + user.hasHighlightTimelineAdministrationPermission + ? () => { + getHttpHighlightClient() + [timeline.isHighlight ? "delete" : "put"](timeline.name) + .catch(() => { + pushAlert({ + message: timeline.isHighlight + ? "timeline.removeHighlightFail" + : "timeline.addHighlightFail", + type: "danger", + }); + }); + } + : undefined + } + /> + {user != null ? ( + <i + className={clsx( + timeline.isBookmark ? "bi-bookmark-fill" : "bi-bookmark", + "icon-button text-yellow mr-3" + )} + onClick={() => { + getHttpBookmarkClient() + [timeline.isBookmark ? "delete" : "put"](timeline.name) + .catch(() => { + pushAlert({ + message: timeline.isBookmark + ? "timeline.removeBookmarkFail" + : "timeline.addBookmarkFail", + type: "danger", + }); + }); + }} + /> + ) : null} + {manageArea.type === "manage" ? ( + <Dropdown className="d-inline-block"> + <Dropdown.Toggle variant="outline-primary"> + {t("timeline.manage")} + </Dropdown.Toggle> + <Dropdown.Menu> + {manageArea.items.map((item, index) => { + if (item.type === "divider") { + return <Dropdown.Divider key={index} />; + } else { + return ( + <Dropdown.Item + key={index} + onClick={item.onClick} + className={ + item.color != null + ? "text-" + item.color + : undefined + } + > + {t(item.text)} + </Dropdown.Item> + ); + } + })} + </Dropdown.Menu> + </Dropdown> + ) : ( + <Button + variant="outline-primary" + onClick={() => setDialog("member")} + > + {t("timeline.memberButton")} + </Button> + )} + </div> + </div> + </div> + {(() => { + if (dialog === "member") { + return ( + <TimelineMemberDialog + timeline={timeline} + onClose={() => setDialog(null)} + open + onChange={onReload} + /> + ); + } else if (dialog === "property") { + return ( + <TimelinePropertyChangeDialog + timeline={timeline} + close={() => setDialog(null)} + open + onChange={onReload} + /> + ); + } + })()} + </> + ); +}; + +export default TimelinePageCardTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx index 92eb0887..3087c20e 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -1,33 +1,39 @@ import React from "react"; - -import { UiLogicError } from "@/common"; +import { useTranslation } from "react-i18next"; +import { Spinner } from "react-bootstrap"; import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; - -import { TimelineMemberDialog } from "./TimelineMember"; -import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; -import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; - -export interface TimelinePageTemplateProps<TManageItem> { - name: string; - onManage: (item: TManageItem) => void; - UiComponent: React.ComponentType< - Omit<TimelinePageTemplateUIProps<TManageItem>, "CardComponent"> - >; +import { + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePostInfo, +} from "@/http/timeline"; + +import { getAlertHost } from "@/services/alert"; + +import Timeline from "./Timeline"; +import TimelinePostEdit from "./TimelinePostEdit"; + +export interface TimelinePageCardProps { + timeline: HttpTimelineInfo; + collapse: boolean; + toggleCollapse: () => void; + className?: string; + onReload: () => void; +} + +export interface TimelinePageTemplateProps { + timelineName: string; notFoundI18nKey: string; reloadKey: number; onReload: () => void; + CardComponent: React.ComponentType<TimelinePageCardProps>; } -export default function TimelinePageTemplate<TManageItem>( - props: TimelinePageTemplateProps<TManageItem> -): React.ReactElement | null { - const { name, reloadKey, onReload } = props; +const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => { + const { timelineName, reloadKey, onReload, CardComponent } = props; - const [dialog, setDialog] = React.useState<null | "property" | "member">( - null - ); + const { t } = useTranslation(); const [timeline, setTimeline] = React.useState< HttpTimelineInfo | "loading" | "offline" | "notexist" | "error" @@ -38,7 +44,7 @@ export default function TimelinePageTemplate<TManageItem>( let subscribe = true; void getHttpTimelineClient() - .getTimeline(name) + .getTimeline(timelineName) .then( (data) => { if (subscribe) { @@ -61,70 +67,117 @@ export default function TimelinePageTemplate<TManageItem>( return () => { subscribe = false; }; - }, [name, reloadKey]); + }, [timelineName, reloadKey]); - let dialogElement: React.ReactElement | undefined; - const closeDialog = (): void => setDialog(null); + const scrollToBottom = React.useCallback(() => { + window.scrollTo(0, document.body.scrollHeight); + }, []); - if (dialog === "property") { - if (typeof timeline !== "object") { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); + const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0); + + const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0); + + const [newPosts, setNewPosts] = React.useState<HttpTimelinePostInfo[]>([]); + + const reloadTimeline = (): void => { + setTimelineReloadKey((old) => old + 1); + setNewPosts([]); + }; + + const onPostEditHeightChange = React.useCallback((height: number): void => { + setBottomSpaceHeight(height); + if (height === 0) { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.removeProperty("margin-bottom"); + } + } else { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.marginBottom = `${height}px`; + } } + }, []); + + const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`; - dialogElement = ( - <TimelinePropertyChangeDialog - open - close={closeDialog} - timeline={timeline} - onChange={onReload} - /> + const [cardCollapse, setCardCollapse] = React.useState<boolean>(true); + React.useEffect(() => { + const savedCollapse = + window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; + setCardCollapse(savedCollapse); + }, [cardCollapseLocalStorageKey]); + + const toggleCardCollapse = (): void => { + const newState = !cardCollapse; + setCardCollapse(newState); + window.localStorage.setItem( + cardCollapseLocalStorageKey, + newState.toString() ); - } else if (dialog === "member") { - if (typeof timeline !== "object") { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); - } + }; + + let body: React.ReactElement; - dialogElement = ( - <TimelineMemberDialog - open - onClose={closeDialog} - timeline={timeline} - onChange={onReload} - /> + if (timeline == "loading") { + body = ( + <div className="full-viewport-center-child"> + <Spinner variant="primary" animation="grow" /> + </div> + ); + } else if (timeline === "offline") { + // TODO: i18n + body = <p className="text-danger">Offline!</p>; + } else if (timeline === "notexist") { + body = <p className="text-danger">{t(props.notFoundI18nKey)}</p>; + } else if (timeline === "error") { + // TODO: i18n + body = <p className="text-danger">Error!</p>; + } else { + body = ( + <> + <CardComponent + className="timeline-template-card" + timeline={timeline} + collapse={cardCollapse} + toggleCollapse={toggleCardCollapse} + onReload={onReload} + /> + <div + className="timeline-container" + style={{ + minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, + }} + > + <Timeline + top={40} + timelineName={timeline.name} + reloadKey={timelineReloadKey} + onReload={reloadTimeline} + additionalPosts={newPosts} + onLoad={scrollToBottom} + /> + </div> + {timeline.postable ? ( + <> + <div + style={{ height: bottomSpaceHeight }} + className="flex-fix-length" + /> + <TimelinePostEdit + className="fixed-bottom" + timeline={timeline} + onHeightChange={onPostEditHeightChange} + onPosted={(newPost) => { + setNewPosts((old) => [...old, newPost]); + }} + /> + </> + ) : null} + </> ); } + return body; +}; - const { UiComponent } = props; - - return ( - <> - <UiComponent - timeline={ - typeof timeline === "object" - ? { - ...timeline, - operations: { - onManage: timeline.manageable - ? (item) => { - if (item === "property") { - setDialog(item); - } else { - props.onManage(item); - } - } - : undefined, - onMember: () => setDialog("member"), - }, - } - : timeline - } - notExistMessageI18nKey={props.notFoundI18nKey} - /> - {dialogElement} - </> - ); -} +export default TimelinePageTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx deleted file mode 100644 index 7319d84d..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Spinner } from "react-bootstrap"; - -import { getAlertHost } from "@/services/alert"; - -import { HttpTimelineInfo, HttpTimelinePostInfo } from "@/http/timeline"; - -import Timeline from "./Timeline"; -import TimelinePostEdit from "./TimelinePostEdit"; - -export interface TimelineCardComponentProps<TManageItems> { - timeline: HttpTimelineInfo; - operations: { - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - }; - collapse: boolean; - toggleCollapse: () => void; - className?: string; -} - -export interface TimelinePageTemplateUIOperations<TManageItems> { - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - onBookmark?: () => void; - onHighlight?: () => void; -} - -export interface TimelinePageTemplateUIProps<TManageItems> { - timeline: - | (HttpTimelineInfo & { - operations: TimelinePageTemplateUIOperations<TManageItems>; - }) - | "notexist" - | "offline" - | "loading" - | "error"; - notExistMessageI18nKey: string; - CardComponent: React.ComponentType<TimelineCardComponentProps<TManageItems>>; -} - -export default function TimelinePageTemplateUI<TManageItems>( - props: TimelinePageTemplateUIProps<TManageItems> -): React.ReactElement | null { - const { timeline, CardComponent } = props; - - const { t } = useTranslation(); - - const scrollToBottom = React.useCallback(() => { - window.scrollTo(0, document.body.scrollHeight); - }, []); - - const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0); - - const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0); - - const [newPosts, setNewPosts] = React.useState<HttpTimelinePostInfo[]>([]); - - const reloadTimeline = (): void => { - setTimelineReloadKey((old) => old + 1); - setNewPosts([]); - }; - - const onPostEditHeightChange = React.useCallback((height: number): void => { - setBottomSpaceHeight(height); - if (height === 0) { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.removeProperty("margin-bottom"); - } - } else { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.marginBottom = `${height}px`; - } - } - }, []); - - const timelineName = typeof timeline === "object" ? timeline.name : null; - - const cardCollapseLocalStorageKey = - timelineName != null ? `timeline.${timelineName}.cardCollapse` : null; - - const [cardCollapse, setCardCollapse] = React.useState<boolean>(true); - React.useEffect(() => { - if (cardCollapseLocalStorageKey != null) { - const savedCollapse = - window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; - setCardCollapse(savedCollapse); - } - }, [cardCollapseLocalStorageKey]); - - const toggleCardCollapse = (): void => { - const newState = !cardCollapse; - setCardCollapse(newState); - if (cardCollapseLocalStorageKey != null) { - window.localStorage.setItem( - cardCollapseLocalStorageKey, - newState.toString() - ); - } - }; - - let body: React.ReactElement; - - if (timeline == "loading") { - body = ( - <div className="full-viewport-center-child"> - <Spinner variant="primary" animation="grow" /> - </div> - ); - } else if (timeline === "offline") { - // TODO: i18n - body = <p className="text-danger">Offline!</p>; - } else if (timeline === "notexist") { - body = <p className="text-danger">{t(props.notExistMessageI18nKey)}</p>; - } else if (timeline === "error") { - // TODO: i18n - body = <p className="text-danger">Error!</p>; - } else { - const { operations } = timeline; - body = ( - <> - <CardComponent - className="timeline-template-card" - timeline={timeline} - operations={operations} - collapse={cardCollapse} - toggleCollapse={toggleCardCollapse} - /> - <div - className="timeline-container" - style={{ - minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, - }} - > - <Timeline - top={40} - timelineName={timeline.name} - reloadKey={timelineReloadKey} - onReload={reloadTimeline} - additionalPosts={newPosts} - onLoad={scrollToBottom} - /> - </div> - {timeline.postable ? ( - <> - <div - style={{ height: bottomSpaceHeight }} - className="flex-fix-length" - /> - <TimelinePostEdit - className="fixed-bottom" - timeline={timeline} - onHeightChange={onPostEditHeightChange} - onPosted={(newPost) => { - setNewPosts((old) => [...old, newPost]); - }} - /> - </> - ) : null} - </> - ); - } - return body; -} diff --git a/FrontEnd/src/app/views/timeline/TimelineCard.tsx b/FrontEnd/src/app/views/timeline/TimelineCard.tsx new file mode 100644 index 00000000..a777cbbd --- /dev/null +++ b/FrontEnd/src/app/views/timeline/TimelineCard.tsx @@ -0,0 +1,85 @@ +import React from "react"; + +import TimelinePageCardTemplate, { + TimelineCardTemplateProps, +} from "../timeline-common/TimelinePageCardTemplate"; +import { TimelinePageCardProps } from "../timeline-common/TimelinePageTemplate"; +import UserAvatar from "../common/user/UserAvatar"; +import TimelineDeleteDialog from "./TimelineDeleteDialog"; + +const TimelineCard: React.FC<TimelinePageCardProps> = (props) => { + const { timeline } = props; + + const [dialog, setDialog] = React.useState< + "member" | "property" | "delete" | null + >(null); + + return ( + <> + <TimelinePageCardTemplate + infoArea={ + <> + <h3 className="text-primary d-inline-block align-middle"> + {timeline.title} + <small className="ml-3 text-secondary">{timeline.name}</small> + </h3> + <div className="align-middle"> + <UserAvatar + username={timeline.owner.username} + className="avatar small rounded-circle mr-3" + /> + {timeline.owner.nickname} + <small className="ml-3 text-secondary"> + @{timeline.owner.username} + </small> + </div> + </> + } + manageArea={((): TimelineCardTemplateProps["manageArea"] => { + if (!timeline.manageable) { + return { type: "member" }; + } else { + return { + type: "manage", + items: [ + { + type: "button", + text: "timeline.manageItem.property", + onClick: () => setDialog("property"), + }, + { + type: "button", + onClick: () => setDialog("member"), + text: "timeline.manageItem.member", + }, + { type: "divider" }, + { + type: "button", + onClick: () => setDialog("delete"), + color: "danger", + text: "timeline.manageItem.delete", + }, + ], + }; + } + })()} + dialog={dialog} + setDialog={setDialog} + {...props} + /> + {(() => { + if (dialog === "delete") { + return ( + <TimelineDeleteDialog + timeline={timeline} + open + close={() => setDialog(null)} + /> + ); + } + })()} + </> + ); +}; + +export default TimelineCard; diff --git a/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx index f472c16a..dbca62ca 100644 --- a/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx @@ -2,20 +2,20 @@ import React from "react"; import { useHistory } from "react-router"; import { Trans } from "react-i18next"; -import { getHttpTimelineClient } from "@/http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; interface TimelineDeleteDialog { + timeline: HttpTimelineInfo; open: boolean; - name: string; close: () => void; } const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => { const history = useHistory(); - const { name } = props; + const { timeline } = props; return ( <OperationDialog @@ -36,14 +36,14 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => { }, ]} inputValidator={([value]) => { - if (value !== name) { + if (value !== timeline.name) { return { 0: "timeline.deleteDialog.notMatch" }; } else { return null; } }} onProcess={() => { - return getHttpTimelineClient().deleteTimeline(name); + return getHttpTimelineClient().deleteTimeline(timeline.name); }} onSuccessAndClose={() => { history.replace("/"); diff --git a/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx b/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx deleted file mode 100644 index 63da6f3c..00000000 --- a/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; - -import TimelineCardTemplate, { - TimelineCardTemplateProps, -} from "../timeline-common/TimelineCardTemplate"; -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import UserAvatar from "../common/user/UserAvatar"; - -export type OrdinaryTimelineManageItem = "delete"; - -export type TimelineInfoCardProps = TimelineCardComponentProps<OrdinaryTimelineManageItem>; - -const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { - const { timeline, operations } = props; - const { onManage, onMember } = operations; - - return ( - <TimelineCardTemplate - infoArea={ - <> - <h3 className="text-primary d-inline-block align-middle"> - {timeline.title} - <small className="ml-3 text-secondary">{timeline.name}</small> - </h3> - <div className="align-middle"> - <UserAvatar - username={timeline.owner.username} - className="avatar small rounded-circle mr-3" - /> - {timeline.owner.nickname} - <small className="ml-3 text-secondary"> - @{timeline.owner.username} - </small> - </div> - </> - } - manageArea={((): TimelineCardTemplateProps["manageArea"] => { - if (onManage == null) { - return { type: "member", onMember }; - } else { - return { - type: "manage", - items: [ - { - type: "button", - text: "timeline.manageItem.property", - onClick: () => onManage("property"), - }, - { - type: "button", - onClick: onMember, - text: "timeline.manageItem.member", - }, - { type: "divider" }, - { - type: "button", - onClick: () => onManage("delete"), - color: "danger", - text: "timeline.manageItem.delete", - }, - ], - }; - } - })()} - {...props} - /> - ); -}; - -export default TimelineInfoCard; diff --git a/FrontEnd/src/app/views/timeline/TimelinePageUI.tsx b/FrontEnd/src/app/views/timeline/TimelinePageUI.tsx deleted file mode 100644 index 67ea699e..00000000 --- a/FrontEnd/src/app/views/timeline/TimelinePageUI.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -import TimelinePageTemplateUI, { - TimelinePageTemplateUIProps, -} from "../timeline-common/TimelinePageTemplateUI"; - -import TimelineInfoCard, { - OrdinaryTimelineManageItem, -} from "./TimelineInfoCard"; - -export type TimelinePageUIProps = Omit< - TimelinePageTemplateUIProps<OrdinaryTimelineManageItem>, - "CardComponent" ->; - -const TimelinePageUI: React.FC<TimelinePageUIProps> = (props) => { - return <TimelinePageTemplateUI {...props} CardComponent={TimelineInfoCard} />; -}; - -export default TimelinePageUI; diff --git a/FrontEnd/src/app/views/timeline/index.tsx b/FrontEnd/src/app/views/timeline/index.tsx index 8048dd12..c5bfd7ab 100644 --- a/FrontEnd/src/app/views/timeline/index.tsx +++ b/FrontEnd/src/app/views/timeline/index.tsx @@ -2,38 +2,21 @@ import React from "react"; import { useParams } from "react-router"; import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; - -import TimelinePageUI from "./TimelinePageUI"; -import { OrdinaryTimelineManageItem } from "./TimelineInfoCard"; -import TimelineDeleteDialog from "./TimelineDeleteDialog"; +import TimelineCard from "./TimelineCard"; const TimelinePage: React.FC = () => { const { name } = useParams<{ name: string }>(); - const [dialog, setDialog] = React.useState<OrdinaryTimelineManageItem | null>( - null - ); const [reloadKey, setReloadKey] = React.useState<number>(0); - let dialogElement: React.ReactElement | undefined; - if (dialog === "delete") { - dialogElement = ( - <TimelineDeleteDialog open close={() => setDialog(null)} name={name} /> - ); - } - return ( - <> - <TimelinePageTemplate - name={name} - UiComponent={TimelinePageUI} - onManage={(item) => setDialog(item)} - notFoundI18nKey="timeline.timelineNotExist" - reloadKey={reloadKey} - onReload={() => setReloadKey(reloadKey + 1)} - /> - {dialogElement} - </> + <TimelinePageTemplate + timelineName={name} + notFoundI18nKey="timeline.timelineNotExist" + reloadKey={reloadKey} + CardComponent={TimelineCard} + onReload={() => setReloadKey(reloadKey + 1)} + /> ); }; diff --git a/FrontEnd/src/app/views/user/UserCard.tsx b/FrontEnd/src/app/views/user/UserCard.tsx new file mode 100644 index 00000000..575ca2c1 --- /dev/null +++ b/FrontEnd/src/app/views/user/UserCard.tsx @@ -0,0 +1,107 @@ +import React from "react"; + +import TimelinePageCardTemplate, { + TimelineCardTemplateProps, +} from "../timeline-common/TimelinePageCardTemplate"; +import { TimelinePageCardProps } from "../timeline-common/TimelinePageTemplate"; +import UserAvatar from "../common/user/UserAvatar"; +import ChangeNicknameDialog from "./ChangeNicknameDialog"; +import { getHttpUserClient } from "@/http/user"; +import ChangeAvatarDialog from "./ChangeAvatarDialog"; + +const UserCard: React.FC<TimelinePageCardProps> = (props) => { + const { timeline, onReload } = props; + + const [dialog, setDialog] = React.useState< + "member" | "property" | "avatar" | "nickname" | null + >(null); + + return ( + <> + <TimelinePageCardTemplate + infoArea={ + <> + <h3 className="text-primary d-inline-block align-middle"> + {timeline.title} + <small className="ml-3 text-secondary">{timeline.name}</small> + </h3> + <div className="align-middle"> + <UserAvatar + username={timeline.owner.username} + className="avatar small rounded-circle mr-3" + /> + {timeline.owner.nickname} + </div> + </> + } + manageArea={((): TimelineCardTemplateProps["manageArea"] => { + if (!timeline.manageable) { + return { type: "member" }; + } else { + return { + type: "manage", + items: [ + { + type: "button", + text: "timeline.manageItem.nickname", + onClick: () => setDialog("nickname"), + }, + { + type: "button", + text: "timeline.manageItem.avatar", + onClick: () => setDialog("avatar"), + }, + { + type: "button", + text: "timeline.manageItem.property", + onClick: () => setDialog("property"), + }, + { + type: "button", + text: "timeline.manageItem.member", + onClick: () => setDialog("member"), + }, + ], + }; + } + })()} + dialog={dialog} + setDialog={setDialog} + {...props} + /> + {(() => { + // TODO: Move this two to settings. + if (dialog === "nickname") { + return ( + <ChangeNicknameDialog + open + close={() => setDialog(null)} + onProcess={async (newNickname) => { + await getHttpUserClient().patch(timeline.owner.username, { + nickname: newNickname, + }); + onReload(); + }} + /> + ); + } else if (dialog === "avatar") { + return ( + <ChangeAvatarDialog + open + close={() => setDialog(null)} + process={async (file) => { + await getHttpUserClient().putAvatar( + timeline.owner.username, + file + ); + onReload(); + }} + /> + ); + } + })()} + </> + ); +}; + +export default UserCard; diff --git a/FrontEnd/src/app/views/user/UserInfoCard.tsx b/FrontEnd/src/app/views/user/UserInfoCard.tsx deleted file mode 100644 index 24b7b979..00000000 --- a/FrontEnd/src/app/views/user/UserInfoCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; - -import TimelineCardTemplate, { - TimelineCardTemplateProps, -} from "../timeline-common/TimelineCardTemplate"; -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import UserAvatar from "../common/user/UserAvatar"; - -export type PersonalTimelineManageItem = "avatar" | "nickname"; - -export type UserInfoCardProps = TimelineCardComponentProps<PersonalTimelineManageItem>; - -const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { - const { timeline, operations } = props; - const { onManage, onMember } = operations; - - return ( - <TimelineCardTemplate - infoArea={ - <> - <h3 className="text-primary d-inline-block align-middle"> - {timeline.title} - <small className="ml-3 text-secondary">{timeline.name}</small> - </h3> - <div className="align-middle"> - <UserAvatar - username={timeline.owner.username} - className="avatar small rounded-circle mr-3" - /> - {timeline.owner.nickname} - </div> - </> - } - manageArea={((): TimelineCardTemplateProps["manageArea"] => { - if (onManage == null) { - return { type: "member", onMember }; - } else { - return { - type: "manage", - items: [ - { - type: "button", - text: "timeline.manageItem.nickname", - onClick: () => onManage("nickname"), - }, - { - type: "button", - text: "timeline.manageItem.avatar", - onClick: () => onManage("avatar"), - }, - { - type: "button", - text: "timeline.manageItem.property", - onClick: () => onManage("property"), - }, - { - type: "button", - onClick: onMember, - text: "timeline.manageItem.member", - }, - ], - }; - } - })()} - {...props} - /> - ); -}; - -export default UserInfoCard; diff --git a/FrontEnd/src/app/views/user/UserPageUI.tsx b/FrontEnd/src/app/views/user/UserPageUI.tsx deleted file mode 100644 index d405399c..00000000 --- a/FrontEnd/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/FrontEnd/src/app/views/user/index.tsx b/FrontEnd/src/app/views/user/index.tsx index 9b5acbba..57454d0d 100644 --- a/FrontEnd/src/app/views/user/index.tsx +++ b/FrontEnd/src/app/views/user/index.tsx @@ -1,58 +1,24 @@ -import React, { useState } from "react"; +import React from "react"; import { useParams } from "react-router"; -import { getHttpUserClient } from "@/http/user"; - import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; -import UserPageUI from "./UserPageUI"; -import { PersonalTimelineManageItem } from "./UserInfoCard"; -import ChangeNicknameDialog from "./ChangeNicknameDialog"; -import ChangeAvatarDialog from "./ChangeAvatarDialog"; +import UserCard from "./UserCard"; -const UserPage: React.FC = (_) => { +const UserPage: React.FC = () => { const { username } = useParams<{ username: string }>(); - const [dialog, setDialog] = useState<null | PersonalTimelineManageItem>(null); - const [reloadKey, setReloadKey] = React.useState<number>(0); let dialogElement: React.ReactElement | undefined; - const closeDialog = (): void => setDialog(null); - - if (dialog === "nickname") { - dialogElement = ( - <ChangeNicknameDialog - open - close={closeDialog} - onProcess={async (newNickname) => { - await getHttpUserClient().patch(username, { nickname: newNickname }); - setReloadKey(reloadKey + 1); - }} - /> - ); - } else if (dialog === "avatar") { - dialogElement = ( - <ChangeAvatarDialog - open - close={closeDialog} - process={async (file) => { - await getHttpUserClient().putAvatar(username, file); - setReloadKey(reloadKey + 1); - }} - /> - ); - } - return ( <> <TimelinePageTemplate - name={`@${username}`} - UiComponent={UserPageUI} - onManage={(item) => setDialog(item)} + timelineName={`@${username}`} notFoundI18nKey="timeline.userNotExist" reloadKey={reloadKey} onReload={() => setReloadKey(reloadKey + 1)} + CardComponent={UserCard} /> {dialogElement} </> |