aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx137
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx173
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx213
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx167
-rw-r--r--FrontEnd/src/app/views/timeline/TimelineCard.tsx85
-rw-r--r--FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx10
-rw-r--r--FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx70
-rw-r--r--FrontEnd/src/app/views/timeline/TimelinePageUI.tsx20
-rw-r--r--FrontEnd/src/app/views/timeline/index.tsx33
-rw-r--r--FrontEnd/src/app/views/user/UserCard.tsx107
-rw-r--r--FrontEnd/src/app/views/user/UserInfoCard.tsx70
-rw-r--r--FrontEnd/src/app/views/user/UserPageUI.tsx18
-rw-r--r--FrontEnd/src/app/views/user/index.tsx44
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}
</>