From aa89b6cce7701a57b0c377d938788b4c940013d6 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 1 Sep 2020 02:32:06 +0800 Subject: ... --- .../app/views/timeline/TimelineDeleteDialog.tsx | 55 +++++++++++ .../src/app/views/timeline/TimelineInfoCard.tsx | 110 +++++++++++++++++++++ .../src/app/views/timeline/TimelinePageUI.tsx | 20 ++++ .../ClientApp/src/app/views/timeline/index.tsx | 37 +++++++ .../ClientApp/src/app/views/timeline/timeline.sass | 14 +++ 5 files changed, 236 insertions(+) create mode 100644 Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx create mode 100644 Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx create mode 100644 Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx create mode 100644 Timeline/ClientApp/src/app/views/timeline/index.tsx create mode 100644 Timeline/ClientApp/src/app/views/timeline/timeline.sass (limited to 'Timeline/ClientApp/src/app/views/timeline') diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx new file mode 100644 index 00000000..894b8195 --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { useHistory } from "react-router"; +import { Trans } from "react-i18next"; + +import { timelineService } from "@/services/timeline"; + +import OperationDialog from "../common/OperationDialog"; + +interface TimelineDeleteDialog { + open: boolean; + name: string; + close: () => void; +} + +const TimelineDeleteDialog: React.FC = (props) => { + const history = useHistory(); + + const { name } = props; + + return ( + { + return ( + + 0{{ name }}2 + + ); + }} + inputScheme={[ + { + type: "text", + validator: (value) => { + if (value !== name) { + return "timeline.deleteDialog.notMatch"; + } else { + return null; + } + }, + }, + ]} + onProcess={() => { + return timelineService.deleteTimeline(name).toPromise(); + }} + onSuccessAndClose={() => { + history.replace("/"); + }} + /> + ); +}; + +export default TimelineDeleteDialog; diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx new file mode 100644 index 00000000..e3e89057 --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import clsx from "clsx"; +import { + Dropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + Button, +} from "reactstrap"; +import { useTranslation } from "react-i18next"; +import { fromEvent } from "rxjs"; + +import { useAvatar } from "@/services/user"; +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; + +import BlobImage from "../common/BlobImage"; +import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; + +export type OrdinaryTimelineManageItem = "delete"; + +export type TimelineInfoCardProps = TimelineCardComponentProps< + OrdinaryTimelineManageItem +>; + +const TimelineInfoCard: React.FC = (props) => { + const { onHeight, onManage } = props; + + const { t } = useTranslation(); + + const avatar = useAvatar(props.timeline.owner.username); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const containerRef = React.useRef(null!); + + const notifyHeight = React.useCallback((): void => { + if (onHeight) { + onHeight(containerRef.current.getBoundingClientRect().height); + } + }, [onHeight]); + + React.useEffect(() => { + const subscription = fromEvent(window, "resize").subscribe(notifyHeight); + return () => subscription.unsubscribe(); + }); + + const [manageDropdownOpen, setManageDropdownOpen] = React.useState( + false + ); + const toggleManageDropdown = React.useCallback( + (): void => setManageDropdownOpen((old) => !old), + [] + ); + + return ( +
+

+ {props.timeline.name} +

+
+ + {props.timeline.owner.nickname} + + @{props.timeline.owner.username} + +
+

{props.timeline.description}

+ + {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])} + +
+ {onManage != null ? ( + + + {t("timeline.manage")} + + + onManage("property")}> + {t("timeline.manageItem.property")} + + + {t("timeline.manageItem.member")} + + + onManage("delete")} + > + {t("timeline.manageItem.delete")} + + + + ) : ( + + )} +
+
+ ); +}; + +export default TimelineInfoCard; diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx new file mode 100644 index 00000000..67ea699e --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import TimelinePageTemplateUI, { + TimelinePageTemplateUIProps, +} from "../timeline-common/TimelinePageTemplateUI"; + +import TimelineInfoCard, { + OrdinaryTimelineManageItem, +} from "./TimelineInfoCard"; + +export type TimelinePageUIProps = Omit< + TimelinePageTemplateUIProps, + "CardComponent" +>; + +const TimelinePageUI: React.FC = (props) => { + return ; +}; + +export default TimelinePageUI; diff --git a/Timeline/ClientApp/src/app/views/timeline/index.tsx b/Timeline/ClientApp/src/app/views/timeline/index.tsx new file mode 100644 index 00000000..225a1a59 --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline/index.tsx @@ -0,0 +1,37 @@ +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"; + +const TimelinePage: React.FC = (_) => { + const { name } = useParams<{ name: string }>(); + + const [dialog, setDialog] = React.useState( + null + ); + + let dialogElement: React.ReactElement | undefined; + if (dialog === "delete") { + dialogElement = ( + setDialog(null)} name={name} /> + ); + } + + return ( + <> + setDialog(item)} + notFoundI18nKey="timeline.timelineNotExist" + /> + {dialogElement} + + ); +}; + +export default TimelinePage; diff --git a/Timeline/ClientApp/src/app/views/timeline/timeline.sass b/Timeline/ClientApp/src/app/views/timeline/timeline.sass new file mode 100644 index 00000000..0eeec73a --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline/timeline.sass @@ -0,0 +1,14 @@ +.info-card-container + .info-card-collapse-button + z-index: 1 + position: relative + + .info-card-content + width: 100% + position: absolute + transform-origin: right top + transition: transform 0.5s + + &[data-collapse='true'] + .info-card-content + transform: scale(0) -- cgit v1.2.3 From ac40d9c18e2ac7b4995dac38d5da04a3ea7f1559 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 3 Sep 2020 18:10:02 +0800 Subject: Migrate to react-bootstrap. --- Timeline/ClientApp/package.json | 2 - Timeline/ClientApp/src/app/service-worker.tsx | 10 ++- Timeline/ClientApp/src/app/views/about/index.tsx | 4 +- Timeline/ClientApp/src/app/views/admin/Admin.tsx | 25 +++--- .../ClientApp/src/app/views/admin/UserAdmin.tsx | 47 +++++------ Timeline/ClientApp/src/app/views/common/AppBar.tsx | 2 +- .../ClientApp/src/app/views/common/FileInput.tsx | 36 --------- .../src/app/views/common/OperationDialog.tsx | 93 ++++++++++------------ .../src/app/views/common/alert/AlertHost.tsx | 9 ++- Timeline/ClientApp/src/app/views/login/index.tsx | 53 ++++++------ .../src/app/views/timeline-common/TimelineItem.tsx | 28 +++---- .../app/views/timeline-common/TimelineMember.tsx | 22 ++--- .../views/timeline-common/TimelinePageTemplate.tsx | 6 +- .../timeline-common/TimelinePageTemplateUI.tsx | 18 ++--- .../app/views/timeline-common/TimelinePostEdit.tsx | 23 +++--- .../app/views/timeline-common/timeline-common.sass | 25 ++++-- .../src/app/views/timeline/TimelineInfoCard.tsx | 46 ++++------- .../ClientApp/src/app/views/timeline/timeline.sass | 14 ---- .../src/app/views/user/ChangeAvatarDialog.tsx | 93 ++++++++++------------ .../ClientApp/src/app/views/user/UserInfoCard.tsx | 48 ++++------- 20 files changed, 247 insertions(+), 357 deletions(-) delete mode 100644 Timeline/ClientApp/src/app/views/common/FileInput.tsx (limited to 'Timeline/ClientApp/src/app/views/timeline') diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index 81232c71..5ae6a608 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -27,7 +27,6 @@ "react-router": "^5.2.0", "react-router-bootstrap": "^0.25.0", "react-router-dom": "^5.2.0", - "reactstrap": "^8.5.1", "regenerator-runtime": "^0.13.7", "rxjs": "^6.6.2", "workbox-precaching": "^5.1.3", @@ -75,7 +74,6 @@ "@types/react-router": "^5.1.8", "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "^5.1.5", - "@types/reactstrap": "^8.5.1", "@types/webpack-env": "^1.15.2", "@types/xregexp": "^4.3.0", "@typescript-eslint/eslint-plugin": "^3.10.1", diff --git a/Timeline/ClientApp/src/app/service-worker.tsx b/Timeline/ClientApp/src/app/service-worker.tsx index e629995a..3be54bc1 100644 --- a/Timeline/ClientApp/src/app/service-worker.tsx +++ b/Timeline/ClientApp/src/app/service-worker.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Button } from "reactstrap"; import { useTranslation } from "react-i18next"; +import { Button } from "react-bootstrap"; import { pushAlert } from "./services/alert"; @@ -39,7 +39,11 @@ if ("serviceWorker" in navigator) { return ( <> {t("serviceWorker.externalActivatedPrompt")} - @@ -83,7 +87,7 @@ if ("serviceWorker" in navigator) { return ( <> {t("serviceWorker.upgradePrompt")} - diff --git a/Timeline/ClientApp/src/app/views/about/index.tsx b/Timeline/ClientApp/src/app/views/about/index.tsx index 21c487da..78cffb5f 100644 --- a/Timeline/ClientApp/src/app/views/about/index.tsx +++ b/Timeline/ClientApp/src/app/views/about/index.tsx @@ -23,8 +23,8 @@ const frontendCredits: { url: "https://getbootstrap.com", }, { - name: "reactstrap", - url: "https://reactstrap.github.io", + name: "react-bootstrap", + url: "https://react-bootstrap.github.io", }, { name: "babeljs", diff --git a/Timeline/ClientApp/src/app/views/admin/Admin.tsx b/Timeline/ClientApp/src/app/views/admin/Admin.tsx index 51dc5a3c..e0f59b0f 100644 --- a/Timeline/ClientApp/src/app/views/admin/Admin.tsx +++ b/Timeline/ClientApp/src/app/views/admin/Admin.tsx @@ -1,5 +1,4 @@ import React, { Fragment } from "react"; -import { Nav, NavItem, NavLink } from "reactstrap"; import { Redirect, Route, @@ -7,7 +6,7 @@ import { useRouteMatch, useHistory, } from "react-router"; -import classnames from "classnames"; +import { Nav } from "react-bootstrap"; import AppBar from "../common/AppBar"; import { UserWithToken } from "@/services/user"; @@ -37,27 +36,27 @@ const Admin: React.FC = (props) => {
- {body} diff --git a/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx b/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx index bde6b3af..18b77ca8 100644 --- a/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx +++ b/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx @@ -1,16 +1,13 @@ import React, { useState, useEffect } from "react"; +import axios from "axios"; import { - ListGroupItem, + ListGroup, Row, Col, - UncontrolledDropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, + Dropdown, Spinner, Button, -} from "reactstrap"; -import axios from "axios"; +} from "react-bootstrap"; import OperationDialog from "../common/OperationDialog"; import { User, UserWithToken } from "@/services/user"; @@ -101,7 +98,7 @@ const UserItem: React.FC = (props) => { }; return ( - +

{user.username}

@@ -112,31 +109,31 @@ const UserItem: React.FC = (props) => { - - + + Manage - - - + + + Change Username - - + + Change Password - - + + Change Permission - - + Delete - - - + + +
-
+ ); }; @@ -441,7 +438,7 @@ const UserAdmin: React.FC = (props) => { return ( <> - + ); } else if (step === "process") { body = ( - + {props.processPrompt?.() ?? } - + ); } else { let content: React.ReactNode; @@ -345,12 +332,12 @@ const OperationDialog: React.FC = (props) => { } body = ( <> - {content} - - - + ); } @@ -359,7 +346,7 @@ const OperationDialog: React.FC = (props) => { return ( - = (props) => { } > {title} - + {body} ); diff --git a/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx b/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx index 31c0fb86..c74f18e2 100644 --- a/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx +++ b/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx @@ -1,8 +1,8 @@ import React, { useCallback } from "react"; -import { Alert } from "reactstrap"; import without from "lodash/without"; import concat from "lodash/concat"; import { useTranslation } from "react-i18next"; +import { Alert } from "react-bootstrap"; import { alertService, @@ -37,7 +37,12 @@ export const AutoCloseAlert: React.FC = (props) => { }, [dismissTime, props.close]); return ( - + {(() => { const { message } = alert; if (typeof message === "function") { diff --git a/Timeline/ClientApp/src/app/views/login/index.tsx b/Timeline/ClientApp/src/app/views/login/index.tsx index e53d0002..5d1e8f06 100644 --- a/Timeline/ClientApp/src/app/views/login/index.tsx +++ b/Timeline/ClientApp/src/app/views/login/index.tsx @@ -1,15 +1,7 @@ import React, { Fragment, useState, useEffect } from "react"; import { useHistory } from "react-router"; import { useTranslation } from "react-i18next"; -import { - Label, - FormGroup, - Input, - Form, - FormFeedback, - Spinner, - Button, -} from "reactstrap"; +import { Form, Spinner, Button } from "react-bootstrap"; import { useUser, userService } from "@/services/user"; @@ -84,9 +76,9 @@ const LoginPage: React.FC = (_) => {

{t("welcome")}

- - - + {t("user.username")} + { @@ -94,15 +86,17 @@ const LoginPage: React.FC = (_) => { setUsernameDirty(true); }} value={username} - invalid={usernameDirty && username === ""} + isInvalid={usernameDirty && username === ""} /> {usernameDirty && username === "" && ( - {t("login.emptyUsername")} + + {t("login.emptyUsername")} + )} - - - - + + {t("user.password")} + { setPasswordDirty(true); }} value={password} - invalid={passwordDirty && password === ""} + isInvalid={passwordDirty && password === ""} /> {passwordDirty && password === "" && ( - {t("login.emptyPassword")} + + {t("login.emptyPassword")} + )} - - - + + id="remember-me" type="checkbox" checked={rememberMe} onChange={(e) => { - const v = (e.target as HTMLInputElement).checked; - setRememberMe(v); + setRememberMe(e.target.checked); }} + label={t("user.rememberMe")} /> - - + {error ?

{t(error)}

: null}
{process ? ( - + ) : ( - )} diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx index f2441612..ce371015 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx @@ -1,19 +1,11 @@ import React from "react"; import clsx from "clsx"; -import { - Row, - Col, - Modal, - ModalHeader, - ModalBody, - ModalFooter, - Button, -} from "reactstrap"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import Svg from "react-inlinesvg"; import chevronDownIcon from "bootstrap-icons/icons/chevron-down.svg"; import trashIcon from "bootstrap-icons/icons/trash.svg"; +import { Row, Col, Modal, Button } from "react-bootstrap"; import { useAvatar } from "@/services/user"; import { TimelinePostInfo } from "@/services/timeline"; @@ -28,16 +20,18 @@ const TimelinePostDeleteConfirmDialog: React.FC<{ return ( - - {t("timeline.post.deleteDialog.title")} - - {t("timeline.post.deleteDialog.prompt")} - - - + ); }; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx index 99605922..67a8543a 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx @@ -1,14 +1,6 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Container, - ListGroup, - ListGroupItem, - Modal, - Row, - Col, - Button, -} from "reactstrap"; +import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; import { User, useAvatar } from "@/services/user"; @@ -25,9 +17,9 @@ const TimelineMemberItem: React.FC<{ const avatar = useAvatar(user.username); return ( - + - + @@ -46,7 +38,7 @@ const TimelineMemberItem: React.FC<{ return (