From a314b5350e269676e8c39eda4cc7842751b1a7fc Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Tue, 1 Sep 2020 02:32:06 +0800
Subject: ...

---
 .../src/app/views/user/ChangeAvatarDialog.tsx      | 307 +++++++++++++++++++++
 1 file changed, 307 insertions(+)
 create mode 100644 Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx

(limited to 'Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx')

diff --git a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx
new file mode 100644
index 00000000..1dd2ee8b
--- /dev/null
+++ b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx
@@ -0,0 +1,307 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+  Modal,
+  ModalHeader,
+  Row,
+  Button,
+  ModalBody,
+  ModalFooter,
+} from "reactstrap";
+import { AxiosError } from "axios";
+
+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 toggle = 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 isOpen={props.open} toggle={toggle}>
+      <ModalHeader> {t("userPage.dialogChangeAvatar.title")}</ModalHeader>
+      {(() => {
+        if (state === "select") {
+          return (
+            <>
+              <ModalBody className="container">
+                <Row>{t("userPage.dialogChangeAvatar.prompt.select")}</Row>
+                <Row>
+                  <input type="file" accept="image/*" onChange={onSelectFile} />
+                </Row>
+              </ModalBody>
+              <ModalFooter>
+                <Button color="secondary" onClick={toggle}>
+                  {t("operationDialog.cancel")}
+                </Button>
+              </ModalFooter>
+            </>
+          );
+        } else if (state === "crop") {
+          if (fileUrl == null) {
+            throw new UiLogicError();
+          }
+          return (
+            <>
+              <ModalBody className="container">
+                <Row className="justify-content-center">
+                  <ImageCropper
+                    clip={clip}
+                    onChange={setClip}
+                    imageUrl={fileUrl}
+                    imageElementCallback={setCropImgElement}
+                  />
+                </Row>
+                <Row>{t("userPage.dialogChangeAvatar.prompt.crop")}</Row>
+              </ModalBody>
+              <ModalFooter>
+                <Button color="secondary" onClick={toggle}>
+                  {t("operationDialog.cancel")}
+                </Button>
+                <Button color="secondary" onClick={onCropPrevious}>
+                  {t("operationDialog.previousStep")}
+                </Button>
+                <Button
+                  color="primary"
+                  onClick={onCropNext}
+                  disabled={
+                    cropImgElement == null || clip == null || clip.width === 0
+                  }
+                >
+                  {t("operationDialog.nextStep")}
+                </Button>
+              </ModalFooter>
+            </>
+          );
+        } else if (state === "processcrop") {
+          return (
+            <>
+              <ModalBody className="container">
+                <Row>
+                  {t("userPage.dialogChangeAvatar.prompt.processingCrop")}
+                </Row>
+              </ModalBody>
+              <ModalFooter>
+                <Button color="secondary" onClick={toggle}>
+                  {t("operationDialog.cancel")}
+                </Button>
+                <Button color="secondary" onClick={onPreviewPrevious}>
+                  {t("operationDialog.previousStep")}
+                </Button>
+              </ModalFooter>
+            </>
+          );
+        } else if (state === "preview") {
+          return (
+            <>
+              <ModalBody className="container">
+                {createPreviewRow()}
+                <Row>{t("userPage.dialogChangeAvatar.prompt.preview")}</Row>
+              </ModalBody>
+              <ModalFooter>
+                <Button color="secondary" onClick={toggle}>
+                  {t("operationDialog.cancel")}
+                </Button>
+                <Button color="secondary" onClick={onPreviewPrevious}>
+                  {t("operationDialog.previousStep")}
+                </Button>
+                <Button color="primary" onClick={upload}>
+                  {t("userPage.dialogChangeAvatar.upload")}
+                </Button>
+              </ModalFooter>
+            </>
+          );
+        } else if (state === "uploading") {
+          return (
+            <>
+              <ModalBody className="container">
+                {createPreviewRow()}
+                <Row>{t("userPage.dialogChangeAvatar.prompt.uploading")}</Row>
+              </ModalBody>
+              <ModalFooter></ModalFooter>
+            </>
+          );
+        } else if (state === "success") {
+          return (
+            <>
+              <ModalBody className="container">
+                <Row className="p-4 text-success">
+                  {t("operationDialog.success")}
+                </Row>
+              </ModalBody>
+              <ModalFooter>
+                <Button color="success" onClick={toggle}>
+                  {t("operationDialog.ok")}
+                </Button>
+              </ModalFooter>
+            </>
+          );
+        } else {
+          return (
+            <>
+              <ModalBody className="container">
+                {createPreviewRow()}
+                <Row className="text-danger">{trueMessage}</Row>
+              </ModalBody>
+              <ModalFooter>
+                <Button color="secondary" onClick={toggle}>
+                  {t("operationDialog.cancel")}
+                </Button>
+                <Button color="primary" onClick={upload}>
+                  {t("operationDialog.retry")}
+                </Button>
+              </ModalFooter>
+            </>
+          );
+        }
+      })()}
+    </Modal>
+  );
+};
+
+export default ChangeAvatarDialog;
-- 
cgit v1.2.3


From 70be5235ba90a15b7798a7922382835fd680b1fc Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Thu, 3 Sep 2020 18:10:02 +0800
Subject: Migrate to react-bootstrap.

---
 Timeline/ClientApp/.pnp.js                         | 132 ---------------------
 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 +++-----
 Timeline/ClientApp/yarn.lock                       | 113 +-----------------
 22 files changed, 252 insertions(+), 597 deletions(-)
 delete mode 100644 Timeline/ClientApp/src/app/views/common/FileInput.tsx

(limited to 'Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx')

diff --git a/Timeline/ClientApp/.pnp.js b/Timeline/ClientApp/.pnp.js
index 51c7e7ae..fbd1b188 100644
--- a/Timeline/ClientApp/.pnp.js
+++ b/Timeline/ClientApp/.pnp.js
@@ -109,10 +109,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
         "@types/react-router-dom",
         "npm:5.1.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router-dom%2Fdownload%2F%40types%2Freact-router-dom-5.1.5.tgz"
       ],
-      [
-        "@types/reactstrap",
-        "npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freactstrap%2Fdownload%2F%40types%2Freactstrap-8.5.1.tgz"
-      ],
       [
         "@types/webpack-env",
         "npm:1.15.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fwebpack-env%2Fdownload%2F%40types%2Fwebpack-env-1.15.2.tgz"
@@ -301,10 +297,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
         "react-router-dom",
         "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router-dom%2Fdownload%2Freact-router-dom-5.2.0.tgz"
       ],
-      [
-        "reactstrap",
-        "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freactstrap%2Fdownload%2Freactstrap-8.5.1.tgz"
-      ],
       [
         "regenerator-runtime",
         "npm:0.13.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.13.7.tgz"
@@ -405,7 +397,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@types/react-router", "npm:5.1.8::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router%2Fdownload%2F%40types%2Freact-router-5.1.8.tgz"],
             ["@types/react-router-bootstrap", "npm:0.24.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router-bootstrap%2Fdownload%2F%40types%2Freact-router-bootstrap-0.24.5.tgz"],
             ["@types/react-router-dom", "npm:5.1.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router-dom%2Fdownload%2F%40types%2Freact-router-dom-5.1.5.tgz"],
-            ["@types/reactstrap", "npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freactstrap%2Fdownload%2F%40types%2Freactstrap-8.5.1.tgz"],
             ["@types/webpack-env", "npm:1.15.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fwebpack-env%2Fdownload%2F%40types%2Fwebpack-env-1.15.2.tgz"],
             ["@types/xregexp", "npm:4.3.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fxregexp%2Fdownload%2F%40types%2Fxregexp-4.3.0.tgz"],
             ["@typescript-eslint/eslint-plugin", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:3.10.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40typescript-eslint%2Feslint-plugin%2Fdownload%2F%40typescript-eslint%2Feslint-plugin-3.10.1.tgz"],
@@ -453,7 +444,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["react-router", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router%2Fdownload%2Freact-router-5.2.0.tgz"],
             ["react-router-bootstrap", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:0.25.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router-bootstrap%2Fdownload%2Freact-router-bootstrap-0.25.0.tgz"],
             ["react-router-dom", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router-dom%2Fdownload%2Freact-router-dom-5.2.0.tgz"],
-            ["reactstrap", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freactstrap%2Fdownload%2Freactstrap-8.5.1.tgz"],
             ["regenerator-runtime", "npm:0.13.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.13.7.tgz"],
             ["rxjs", "npm:6.6.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Frxjs%2Fdownload%2Frxjs-6.6.2.tgz"],
             ["sass", "npm:1.26.10::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fsass%2Fdownload%2Fsass-1.26.10.tgz"],
@@ -3523,17 +3513,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD",
         }]
       ]],
-      ["@types/reactstrap", [
-        ["npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freactstrap%2Fdownload%2F%40types%2Freactstrap-8.5.1.tgz", {
-          "packageLocation": "./.yarn/cache/@types-reactstrap-npm-8.5.1-9008a6014f-87b383af72.zip/node_modules/@types/reactstrap/",
-          "packageDependencies": [
-            ["@types/reactstrap", "npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freactstrap%2Fdownload%2F%40types%2Freactstrap-8.5.1.tgz"],
-            ["@types/react", "npm:16.9.43::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-16.9.43.tgz"],
-            ["popper.js", "npm:1.16.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fpopper.js%2Fdownload%2Fpopper.js-1.16.1.tgz"]
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
       ["@types/resolve", [
         ["npm:0.0.8::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz", {
           "packageLocation": "./.yarn/cache/@types-resolve-npm-0.0.8-86c1fa5722-f54f13e4b6.zip/node_modules/@types/resolve/",
@@ -6087,23 +6066,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD",
         }]
       ]],
-      ["create-react-context", [
-        ["virtual:3edf6c5f28de9425dc8acc6598a590fa2241383598df1fae41aa7aced4929105d0fdbdc1efc064936982064228d600f97c6ec4468783d71cc55394fd7509ffb1#npm:0.3.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fcreate-react-context%2Fdownload%2Fcreate-react-context-0.3.0.tgz", {
-          "packageLocation": "./.yarn/$$virtual/create-react-context-virtual-3b1789c663/0/cache/create-react-context-npm-0.3.0-4a7d9dced4-dfb153bbcf.zip/node_modules/create-react-context/",
-          "packageDependencies": [
-            ["create-react-context", "virtual:3edf6c5f28de9425dc8acc6598a590fa2241383598df1fae41aa7aced4929105d0fdbdc1efc064936982064228d600f97c6ec4468783d71cc55394fd7509ffb1#npm:0.3.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fcreate-react-context%2Fdownload%2Fcreate-react-context-0.3.0.tgz"],
-            ["gud", "npm:1.0.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fgud%2Fdownload%2Fgud-1.0.0.tgz"],
-            ["prop-types", "npm:15.7.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fprop-types%2Fdownload%2Fprop-types-15.7.2.tgz"],
-            ["react", "npm:16.13.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact%2Fdownload%2Freact-16.13.1.tgz"],
-            ["warning", "npm:4.0.3::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fwarning%2Fdownload%2Fwarning-4.0.3.tgz"]
-          ],
-          "packagePeers": [
-            "prop-types",
-            "react"
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
       ["cross-spawn", [
         ["npm:6.0.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fcross-spawn%2Fdownload%2Fcross-spawn-6.0.5.tgz", {
           "packageLocation": "./.yarn/cache/cross-spawn-npm-6.0.5-b596e42a26-05fbbf957d.zip/node_modules/cross-spawn/",
@@ -6673,14 +6635,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
         }]
       ]],
       ["dom-helpers", [
-        ["npm:3.4.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-helpers%2Fdownload%2Fdom-helpers-3.4.0.tgz", {
-          "packageLocation": "./.yarn/cache/dom-helpers-npm-3.4.0-f3b5be067d-7625598dbf.zip/node_modules/dom-helpers/",
-          "packageDependencies": [
-            ["dom-helpers", "npm:3.4.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-helpers%2Fdownload%2Fdom-helpers-3.4.0.tgz"],
-            ["@babel/runtime", "npm:7.10.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.10.5.tgz"]
-          ],
-          "linkType": "HARD",
-        }],
         ["npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-helpers%2Fdownload%2Fdom-helpers-5.2.0.tgz", {
           "packageLocation": "./.yarn/cache/dom-helpers-npm-5.2.0-50f26cbd58-9ef27628f4.zip/node_modules/dom-helpers/",
           "packageDependencies": [
@@ -8259,15 +8213,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD",
         }]
       ]],
-      ["gud", [
-        ["npm:1.0.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fgud%2Fdownload%2Fgud-1.0.0.tgz", {
-          "packageLocation": "./.yarn/cache/gud-npm-1.0.0-e29c6f7e18-08be6bf30e.zip/node_modules/gud/",
-          "packageDependencies": [
-            ["gud", "npm:1.0.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fgud%2Fdownload%2Fgud-1.0.0.tgz"]
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
       ["handle-thing", [
         ["npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fhandle-thing%2Fdownload%2Fhandle-thing-2.0.1.tgz", {
           "packageLocation": "./.yarn/cache/handle-thing-npm-2.0.1-05158d71e6-7509fca9eb.zip/node_modules/handle-thing/",
@@ -11401,15 +11346,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD",
         }]
       ]],
-      ["popper.js", [
-        ["npm:1.16.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fpopper.js%2Fdownload%2Fpopper.js-1.16.1.tgz", {
-          "packageLocation": "./.yarn/cache/popper.js-npm-1.16.1-45dd821913-eb53806fb7.zip/node_modules/popper.js/",
-          "packageDependencies": [
-            ["popper.js", "npm:1.16.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fpopper.js%2Fdownload%2Fpopper.js-1.16.1.tgz"]
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
       ["portfinder", [
         ["npm:1.0.27::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fportfinder%2Fdownload%2Fportfinder-1.0.27.tgz", {
           "packageLocation": "./.yarn/cache/portfinder-npm-1.0.27-519aa211df-dfecedbaf8.zip/node_modules/portfinder/",
@@ -12448,26 +12384,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD",
         }]
       ]],
-      ["react-popper", [
-        ["virtual:3b2ce7b5c8060bb05f2f5c2935213be0b80cbf02d12c698b50272c06d2940bcce3e52fed2f822d06cfa7b13a6aae64654f797a522ecaeb419458c4c10e644495#npm:1.3.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-popper%2Fdownload%2Freact-popper-1.3.7.tgz", {
-          "packageLocation": "./.yarn/$$virtual/react-popper-virtual-3edf6c5f28/0/cache/react-popper-npm-1.3.7-0019173f32-09ef58054b.zip/node_modules/react-popper/",
-          "packageDependencies": [
-            ["react-popper", "virtual:3b2ce7b5c8060bb05f2f5c2935213be0b80cbf02d12c698b50272c06d2940bcce3e52fed2f822d06cfa7b13a6aae64654f797a522ecaeb419458c4c10e644495#npm:1.3.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-popper%2Fdownload%2Freact-popper-1.3.7.tgz"],
-            ["@babel/runtime", "npm:7.10.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.10.5.tgz"],
-            ["create-react-context", "virtual:3edf6c5f28de9425dc8acc6598a590fa2241383598df1fae41aa7aced4929105d0fdbdc1efc064936982064228d600f97c6ec4468783d71cc55394fd7509ffb1#npm:0.3.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fcreate-react-context%2Fdownload%2Fcreate-react-context-0.3.0.tgz"],
-            ["deep-equal", "npm:1.1.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeep-equal%2Fdownload%2Fdeep-equal-1.1.1.tgz"],
-            ["popper.js", "npm:1.16.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fpopper.js%2Fdownload%2Fpopper.js-1.16.1.tgz"],
-            ["prop-types", "npm:15.7.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fprop-types%2Fdownload%2Fprop-types-15.7.2.tgz"],
-            ["react", "npm:16.13.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact%2Fdownload%2Freact-16.13.1.tgz"],
-            ["typed-styles", "npm:0.0.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Ftyped-styles%2Fdownload%2Ftyped-styles-0.0.7.tgz"],
-            ["warning", "npm:4.0.3::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fwarning%2Fdownload%2Fwarning-4.0.3.tgz"]
-          ],
-          "packagePeers": [
-            "react"
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
       ["react-responsive", [
         ["virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:8.1.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-responsive%2Fdownload%2Freact-responsive-8.1.0.tgz", {
           "packageLocation": "./.yarn/$$virtual/react-responsive-virtual-889e4c0da2/0/cache/react-responsive-npm-8.1.0-0c4689f9df-059c460fd4.zip/node_modules/react-responsive/",
@@ -12561,43 +12477,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             "react-dom"
           ],
           "linkType": "HARD",
-        }],
-        ["virtual:3b2ce7b5c8060bb05f2f5c2935213be0b80cbf02d12c698b50272c06d2940bcce3e52fed2f822d06cfa7b13a6aae64654f797a522ecaeb419458c4c10e644495#npm:2.9.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-transition-group%2Fdownload%2Freact-transition-group-2.9.0.tgz", {
-          "packageLocation": "./.yarn/$$virtual/react-transition-group-virtual-96734f4ac2/0/cache/react-transition-group-npm-2.9.0-67f8b00a44-eefed08c48.zip/node_modules/react-transition-group/",
-          "packageDependencies": [
-            ["react-transition-group", "virtual:3b2ce7b5c8060bb05f2f5c2935213be0b80cbf02d12c698b50272c06d2940bcce3e52fed2f822d06cfa7b13a6aae64654f797a522ecaeb419458c4c10e644495#npm:2.9.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-transition-group%2Fdownload%2Freact-transition-group-2.9.0.tgz"],
-            ["dom-helpers", "npm:3.4.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-helpers%2Fdownload%2Fdom-helpers-3.4.0.tgz"],
-            ["loose-envify", "npm:1.4.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Floose-envify%2Fdownload%2Floose-envify-1.4.0.tgz"],
-            ["prop-types", "npm:15.7.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fprop-types%2Fdownload%2Fprop-types-15.7.2.tgz"],
-            ["react", "npm:16.13.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact%2Fdownload%2Freact-16.13.1.tgz"],
-            ["react-dom", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:16.13.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-dom%2Fdownload%2Freact-dom-16.13.1.tgz"],
-            ["react-lifecycles-compat", "npm:3.0.4::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-lifecycles-compat%2Fdownload%2Freact-lifecycles-compat-3.0.4.tgz"]
-          ],
-          "packagePeers": [
-            "react",
-            "react-dom"
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
-      ["reactstrap", [
-        ["virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freactstrap%2Fdownload%2Freactstrap-8.5.1.tgz", {
-          "packageLocation": "./.yarn/$$virtual/reactstrap-virtual-3b2ce7b5c8/0/cache/reactstrap-npm-8.5.1-233980b7a4-afa5cba1a9.zip/node_modules/reactstrap/",
-          "packageDependencies": [
-            ["reactstrap", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freactstrap%2Fdownload%2Freactstrap-8.5.1.tgz"],
-            ["@babel/runtime", "npm:7.10.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.10.5.tgz"],
-            ["classnames", "npm:2.2.6::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fclassnames%2Fdownload%2Fclassnames-2.2.6.tgz"],
-            ["prop-types", "npm:15.7.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fprop-types%2Fdownload%2Fprop-types-15.7.2.tgz"],
-            ["react", "npm:16.13.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact%2Fdownload%2Freact-16.13.1.tgz"],
-            ["react-dom", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:16.13.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-dom%2Fdownload%2Freact-dom-16.13.1.tgz"],
-            ["react-popper", "virtual:3b2ce7b5c8060bb05f2f5c2935213be0b80cbf02d12c698b50272c06d2940bcce3e52fed2f822d06cfa7b13a6aae64654f797a522ecaeb419458c4c10e644495#npm:1.3.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-popper%2Fdownload%2Freact-popper-1.3.7.tgz"],
-            ["react-transition-group", "virtual:3b2ce7b5c8060bb05f2f5c2935213be0b80cbf02d12c698b50272c06d2940bcce3e52fed2f822d06cfa7b13a6aae64654f797a522ecaeb419458c4c10e644495#npm:2.9.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-transition-group%2Fdownload%2Freact-transition-group-2.9.0.tgz"]
-          ],
-          "packagePeers": [
-            "react",
-            "react-dom"
-          ],
-          "linkType": "HARD",
         }]
       ]],
       ["read-pkg", [
@@ -14271,7 +14150,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@types/react-router", "npm:5.1.8::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router%2Fdownload%2F%40types%2Freact-router-5.1.8.tgz"],
             ["@types/react-router-bootstrap", "npm:0.24.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router-bootstrap%2Fdownload%2F%40types%2Freact-router-bootstrap-0.24.5.tgz"],
             ["@types/react-router-dom", "npm:5.1.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact-router-dom%2Fdownload%2F%40types%2Freact-router-dom-5.1.5.tgz"],
-            ["@types/reactstrap", "npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freactstrap%2Fdownload%2F%40types%2Freactstrap-8.5.1.tgz"],
             ["@types/webpack-env", "npm:1.15.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fwebpack-env%2Fdownload%2F%40types%2Fwebpack-env-1.15.2.tgz"],
             ["@types/xregexp", "npm:4.3.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fxregexp%2Fdownload%2F%40types%2Fxregexp-4.3.0.tgz"],
             ["@typescript-eslint/eslint-plugin", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:3.10.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40typescript-eslint%2Feslint-plugin%2Fdownload%2F%40typescript-eslint%2Feslint-plugin-3.10.1.tgz"],
@@ -14319,7 +14197,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["react-router", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router%2Fdownload%2Freact-router-5.2.0.tgz"],
             ["react-router-bootstrap", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:0.25.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router-bootstrap%2Fdownload%2Freact-router-bootstrap-0.25.0.tgz"],
             ["react-router-dom", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-router-dom%2Fdownload%2Freact-router-dom-5.2.0.tgz"],
-            ["reactstrap", "virtual:71f98ed0939a4e8e7ea376e302a494701bc5b6aa7a7eb81870139ee3950a7c417a3d13b346b5b526d93952a598dffe628a0fac2148047debade23536cb3d7957#npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freactstrap%2Fdownload%2Freactstrap-8.5.1.tgz"],
             ["regenerator-runtime", "npm:0.13.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.13.7.tgz"],
             ["rxjs", "npm:6.6.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Frxjs%2Fdownload%2Frxjs-6.6.2.tgz"],
             ["sass", "npm:1.26.10::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fsass%2Fdownload%2Fsass-1.26.10.tgz"],
@@ -14610,15 +14487,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD",
         }]
       ]],
-      ["typed-styles", [
-        ["npm:0.0.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Ftyped-styles%2Fdownload%2Ftyped-styles-0.0.7.tgz", {
-          "packageLocation": "./.yarn/cache/typed-styles-npm-0.0.7-ef2e89d04b-4988d78daf.zip/node_modules/typed-styles/",
-          "packageDependencies": [
-            ["typed-styles", "npm:0.0.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Ftyped-styles%2Fdownload%2Ftyped-styles-0.0.7.tgz"]
-          ],
-          "linkType": "HARD",
-        }]
-      ]],
       ["typedarray", [
         ["npm:0.0.6::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypedarray%2Fdownload%2Ftypedarray-0.0.6.tgz", {
           "packageLocation": "./.yarn/cache/typedarray-npm-0.0.6-9f6b6bcac0-c9ef0176aa.zip/node_modules/typedarray/",
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")}
-              <Button color="success" size="sm" onClick={upgradeReload} outline>
+              <Button
+                variant="outline-success"
+                size="sm"
+                onClick={upgradeReload}
+              >
                 {t("serviceWorker.reloadNow")}
               </Button>
             </>
@@ -83,7 +87,7 @@ if ("serviceWorker" in navigator) {
         return (
           <>
             {t("serviceWorker.upgradePrompt")}
-            <Button color="success" size="sm" onClick={upgrade} outline>
+            <Button variant="outline-success" size="sm" onClick={upgrade}>
               {t("serviceWorker.upgradeNow")}
             </Button>
           </>
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<AdminProps> = (props) => {
       <Route path={`${match.path}/${name}`}>
         <AppBar />
         <div style={{ height: 56 }} className="flex-fix-length" />
-        <Nav tabs>
-          <NavItem>
-            <NavLink
-              className={classnames({ active: tabName === "users" })}
+        <Nav variant="tabs">
+          <Nav.Item>
+            <Nav.Link
+              active={tabName === "users"}
               onClick={() => {
                 toggle("users");
               }}
             >
               Users
-            </NavLink>
-          </NavItem>
-          <NavItem>
-            <NavLink
-              className={classnames({ active: tabName === "more" })}
+            </Nav.Link>
+          </Nav.Item>
+          <Nav.Item>
+            <Nav.Link
+              active={tabName === "more"}
               onClick={() => {
                 toggle("more");
               }}
             >
               More
-            </NavLink>
-          </NavItem>
+            </Nav.Link>
+          </Nav.Item>
         </Nav>
         {body}
       </Route>
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<UserCardProps> = (props) => {
   };
 
   return (
-    <ListGroupItem className="container">
+    <ListGroup.Item className="container">
       <Row className="align-items-center">
         <Col>
           <p className="mb-0 text-primary">{user.username}</p>
@@ -112,31 +109,31 @@ const UserItem: React.FC<UserCardProps> = (props) => {
           </small>
         </Col>
         <Col className="col-auto">
-          <UncontrolledDropdown>
-            <DropdownToggle color="warning" className="text-light" caret>
+          <Dropdown>
+            <Dropdown.Toggle variant="warning" className="text-light">
               Manage
-            </DropdownToggle>
-            <DropdownMenu>
-              <DropdownItem onClick={createClickCallback(kChangeUsername)}>
+            </Dropdown.Toggle>
+            <Dropdown.Menu>
+              <Dropdown.Item onClick={createClickCallback(kChangeUsername)}>
                 Change Username
-              </DropdownItem>
-              <DropdownItem onClick={createClickCallback(kChangePassword)}>
+              </Dropdown.Item>
+              <Dropdown.Item onClick={createClickCallback(kChangePassword)}>
                 Change Password
-              </DropdownItem>
-              <DropdownItem onClick={createClickCallback(kChangePermission)}>
+              </Dropdown.Item>
+              <Dropdown.Item onClick={createClickCallback(kChangePermission)}>
                 Change Permission
-              </DropdownItem>
-              <DropdownItem
+              </Dropdown.Item>
+              <Dropdown.Item
                 className="text-danger"
                 onClick={createClickCallback(kDelete)}
               >
                 Delete
-              </DropdownItem>
-            </DropdownMenu>
-          </UncontrolledDropdown>
+              </Dropdown.Item>
+            </Dropdown.Menu>
+          </Dropdown>
         </Col>
       </Row>
-    </ListGroupItem>
+    </ListGroup.Item>
   );
 };
 
@@ -441,7 +438,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
     return (
       <>
         <Button
-          color="success"
+          variant="success"
           onClick={() =>
             setDialog({
               type: "create",
@@ -456,7 +453,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
       </>
     );
   } else {
-    return <Spinner />;
+    return <Spinner animation="border" />;
   }
 };
 
diff --git a/Timeline/ClientApp/src/app/views/common/AppBar.tsx b/Timeline/ClientApp/src/app/views/common/AppBar.tsx
index 464747c0..ee4ead8f 100644
--- a/Timeline/ClientApp/src/app/views/common/AppBar.tsx
+++ b/Timeline/ClientApp/src/app/views/common/AppBar.tsx
@@ -17,7 +17,7 @@ const AppBar: React.FC = (_) => {
   const isAdministrator = user && user.administrator;
 
   return (
-    <Navbar bg="primary" variant="dark" expand="md">
+    <Navbar bg="primary" variant="dark" expand="md" sticky="top">
       <LinkContainer to="/">
         <Navbar.Brand className="d-flex align-items-center">
           <TimelineLogo style={{ height: "1em" }} />
diff --git a/Timeline/ClientApp/src/app/views/common/FileInput.tsx b/Timeline/ClientApp/src/app/views/common/FileInput.tsx
deleted file mode 100644
index 7b053d5c..00000000
--- a/Timeline/ClientApp/src/app/views/common/FileInput.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "react";
-import clsx from "clsx";
-
-export interface FileInputProps
-  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "id"> {
-  inputId?: string;
-  labelText: string;
-  color?: string;
-  className?: string;
-}
-
-const FileInput: React.FC<FileInputProps> = (props) => {
-  const { inputId, labelText, color, className, ...otherProps } = props;
-
-  const realInputId = React.useMemo<string>(() => {
-    if (inputId != null) return inputId;
-    return (
-      "file-input-" +
-      (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
-    );
-  }, [inputId]);
-
-  return (
-    <>
-      <input className="d-none" type="file" id={realInputId} {...otherProps} />
-      <label
-        htmlFor={realInputId}
-        className={clsx("btn", "btn-" + (color ?? "primary"), className)}
-      >
-        {labelText}
-      </label>
-    </>
-  );
-};
-
-export default FileInput;
diff --git a/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx b/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx
index 402ffbec..6f97eb15 100644
--- a/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx
+++ b/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx
@@ -1,26 +1,13 @@
 import React, { useState } from "react";
 import { useTranslation } from "react-i18next";
-import {
-  Spinner,
-  Container,
-  ModalBody,
-  Label,
-  Input,
-  FormGroup,
-  FormFeedback,
-  ModalFooter,
-  Button,
-  Modal,
-  ModalHeader,
-  FormText,
-} from "reactstrap";
+import { Spinner, Container, Form, Button, Modal } from "react-bootstrap";
 
 import { UiLogicError } from "@/common";
 
 const DefaultProcessPrompt: React.FC = (_) => {
   return (
     <Container className="justify-content-center align-items-center">
-      <Spinner />
+      <Spinner animation="border" variant="success" />
     </Container>
   );
 };
@@ -233,7 +220,7 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
 
     body = (
       <>
-        <ModalBody>
+        <Modal.Body>
           {inputPrompt}
           {inputScheme.map((item, index) => {
             const value = values[index];
@@ -242,9 +229,9 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
 
             if (item.type === "text") {
               return (
-                <FormGroup key={index}>
-                  {item.label && <Label>{t(item.label)}</Label>}
-                  <Input
+                <Form.Group key={index}>
+                  {item.label && <Form.Label>{t(item.label)}</Form.Label>}
+                  <Form.Control
                     type={item.password === true ? "password" : "text"}
                     value={value as string}
                     onChange={(e) => {
@@ -258,35 +245,35 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
                         )
                       );
                     }}
-                    invalid={error != null}
-                    {...item.textFieldProps}
+                    isInvalid={error != null}
                   />
-                  {error != null && <FormFeedback>{error}</FormFeedback>}
-                  {item.helperText && <FormText>{t(item.helperText)}</FormText>}
-                </FormGroup>
+                  {error != null && (
+                    <Form.Control.Feedback>{error}</Form.Control.Feedback>
+                  )}
+                  {item.helperText && (
+                    <Form.Text>{t(item.helperText)}</Form.Text>
+                  )}
+                </Form.Group>
               );
             } else if (item.type === "bool") {
               return (
-                <FormGroup check key={index}>
-                  <Input
+                <Form.Group key={index}>
+                  <Form.Check<"input">
                     type="checkbox"
-                    value={value as string}
-                    onChange={(e) => {
-                      updateValue(
-                        index,
-                        (e.target as HTMLInputElement).checked
-                      );
+                    checked={value as boolean}
+                    onChange={(event) => {
+                      updateValue(index, event.currentTarget.checked);
                     }}
+                    label={t(item.label)}
                   />
-                  <Label check>{t(item.label)}</Label>
-                </FormGroup>
+                </Form.Group>
               );
             } else if (item.type === "select") {
               return (
-                <FormGroup key={index}>
-                  <Label>{t(item.label)}</Label>
-                  <Input
-                    type="select"
+                <Form.Group key={index}>
+                  <Form.Label>{t(item.label)}</Form.Label>
+                  <Form.Control
+                    as="select"
                     value={value as string}
                     onChange={(event) => {
                       updateValue(index, event.target.value);
@@ -300,18 +287,18 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
                         </option>
                       );
                     })}
-                  </Input>
-                </FormGroup>
+                  </Form.Control>
+                </Form.Group>
               );
             }
           })}
-        </ModalBody>
-        <ModalFooter>
-          <Button color="secondary" onClick={close}>
+        </Modal.Body>
+        <Modal.Footer>
+          <Button variant="secondary" onClick={close}>
             {t("operationDialog.cancel")}
           </Button>
           <Button
-            color="primary"
+            variant="primary"
             disabled={testErrorInfo(inputError)}
             onClick={() => {
               if (validateAll()) {
@@ -321,14 +308,14 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
           >
             {t("operationDialog.confirm")}
           </Button>
-        </ModalFooter>
+        </Modal.Footer>
       </>
     );
   } else if (step === "process") {
     body = (
-      <ModalBody>
+      <Modal.Body>
         {props.processPrompt?.() ?? <DefaultProcessPrompt />}
-      </ModalBody>
+      </Modal.Body>
     );
   } else {
     let content: React.ReactNode;
@@ -345,12 +332,12 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
     }
     body = (
       <>
-        <ModalBody>{content}</ModalBody>
-        <ModalFooter>
-          <Button color="primary" onClick={close}>
+        <Modal.Body>{content}</Modal.Body>
+        <Modal.Footer>
+          <Button variant="primary" onClick={close}>
             {t("operationDialog.ok")}
           </Button>
-        </ModalFooter>
+        </Modal.Footer>
       </>
     );
   }
@@ -359,7 +346,7 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
 
   return (
     <Modal isOpen={props.open} toggle={close}>
-      <ModalHeader
+      <Modal.Header
         className={
           props.titleColor != null
             ? "text-" +
@@ -372,7 +359,7 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => {
         }
       >
         {title}
-      </ModalHeader>
+      </Modal.Header>
       {body}
     </Modal>
   );
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<AutoCloseAlertProps> = (props) => {
   }, [dismissTime, props.close]);
 
   return (
-    <Alert className="m-3" color={alert.type ?? "primary"} toggle={props.close}>
+    <Alert
+      className="m-3"
+      variant={alert.type ?? "primary"}
+      onClose={props.close}
+      dismissible
+    >
       {(() => {
         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 = (_) => {
       <div className="container login-container mt-appbar">
         <h1>{t("welcome")}</h1>
         <Form>
-          <FormGroup>
-            <Label for="username">{t("user.username")}</Label>
-            <Input
+          <Form.Group>
+            <Form.Label htmlFor="username">{t("user.username")}</Form.Label>
+            <Form.Control
               id="username"
               disabled={process}
               onChange={(e) => {
@@ -94,15 +86,17 @@ const LoginPage: React.FC = (_) => {
                 setUsernameDirty(true);
               }}
               value={username}
-              invalid={usernameDirty && username === ""}
+              isInvalid={usernameDirty && username === ""}
             />
             {usernameDirty && username === "" && (
-              <FormFeedback>{t("login.emptyUsername")}</FormFeedback>
+              <Form.Control.Feedback>
+                {t("login.emptyUsername")}
+              </Form.Control.Feedback>
             )}
-          </FormGroup>
-          <FormGroup>
-            <Label for="password">{t("user.password")}</Label>
-            <Input
+          </Form.Group>
+          <Form.Group>
+            <Form.Label htmlFor="password">{t("user.password")}</Form.Label>
+            <Form.Control
               id="password"
               type="password"
               disabled={process}
@@ -111,30 +105,31 @@ const LoginPage: React.FC = (_) => {
                 setPasswordDirty(true);
               }}
               value={password}
-              invalid={passwordDirty && password === ""}
+              isInvalid={passwordDirty && password === ""}
             />
             {passwordDirty && password === "" && (
-              <FormFeedback>{t("login.emptyPassword")}</FormFeedback>
+              <Form.Control.Feedback>
+                {t("login.emptyPassword")}
+              </Form.Control.Feedback>
             )}
-          </FormGroup>
-          <FormGroup check>
-            <Input
+          </Form.Group>
+          <Form.Group>
+            <Form.Check<"input">
               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")}
             />
-            <Label for="remember-me">{t("user.rememberMe")}</Label>
-          </FormGroup>
+          </Form.Group>
           {error ? <p className="text-error">{t(error)}</p> : null}
           <div>
             {process ? (
-              <Spinner />
+              <Spinner animation="border" />
             ) : (
-              <Button color="primary" onClick={onSubmit}>
+              <Button variant="primary" onClick={onSubmit}>
                 {t("user.login")}
               </Button>
             )}
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 (
     <Modal toggle={toggle} isOpen centered>
-      <ModalHeader className="text-danger">
-        {t("timeline.post.deleteDialog.title")}
-      </ModalHeader>
-      <ModalBody>{t("timeline.post.deleteDialog.prompt")}</ModalBody>
-      <ModalFooter>
-        <Button color="secondary" onClick={toggle}>
+      <Modal.Header>
+        <Modal.Title className="text-danger">
+          {t("timeline.post.deleteDialog.title")}
+        </Modal.Title>
+      </Modal.Header>
+      <Modal.Body>{t("timeline.post.deleteDialog.prompt")}</Modal.Body>
+      <Modal.Footer>
+        <Button variant="secondary" onClick={toggle}>
           {t("operationDialog.cancel")}
         </Button>
         <Button
-          color="danger"
+          variant="danger"
           onClick={() => {
             onConfirm();
             toggle();
@@ -45,7 +39,7 @@ const TimelinePostDeleteConfirmDialog: React.FC<{
         >
           {t("operationDialog.confirm")}
         </Button>
-      </ModalFooter>
+      </Modal.Footer>
     </Modal>
   );
 };
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 (
-    <ListGroupItem className="container">
+    <ListGroup.Item className="container">
       <Row>
-        <Col className="col-auto">
+        <Col xs="auto">
           <BlobImage blob={avatar} className="avatar small" />
         </Col>
         <Col>
@@ -46,7 +38,7 @@ const TimelineMemberItem: React.FC<{
           return (
             <Button
               className="align-self-center"
-              color="danger"
+              variant="danger"
               onClick={() => {
                 onRemove(user.username);
               }}
@@ -56,7 +48,7 @@ const TimelineMemberItem: React.FC<{
           );
         })()}
       </Row>
-    </ListGroupItem>
+    </ListGroup.Item>
   );
 };
 
@@ -169,7 +161,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
                             </Row>
                           </Col>
                           <Button
-                            color="primary"
+                            variant="primary"
                             className="align-self-center"
                             disabled={!addable}
                             onClick={() => {
@@ -212,7 +204,7 @@ export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = (
   props
 ) => {
   return (
-    <Modal isOpen={props.open} toggle={props.onClose}>
+    <Modal show centered onHide={props.onClose}>
       <TimelineMember {...props} />
     </Modal>
   );
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx
index 1b03d5c7..d5c91622 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx
@@ -160,10 +160,6 @@ export default function TimelinePageTemplate<TManageItem>(
     [onManageProp]
   );
 
-  const onMember = React.useCallback(() => {
-    setDialog("member");
-  }, []);
-
   return (
     <>
       <UiComponent
@@ -181,7 +177,7 @@ export default function TimelinePageTemplate<TManageItem>(
             ? onManage
             : undefined
         }
-        onMember={onMember}
+        onMember={() => setDialog("member")}
       />
       {dialogElement}
     </>
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx
index 7af11efa..e25ed962 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx
@@ -1,9 +1,9 @@
 import React, { CSSProperties } from "react";
-import { Spinner } from "reactstrap";
+import clsx from "clsx";
 import { useTranslation } from "react-i18next";
 import { fromEvent } from "rxjs";
 import Svg from "react-inlinesvg";
-import clsx from "clsx";
+import { Spinner } from "react-bootstrap";
 import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg";
 import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg";
 
@@ -262,7 +262,7 @@ export default function TimelinePageTemplateUI<TManageItems>(
       } else {
         timelineBody = (
           <div className="full-viewport-center-child">
-            <Spinner color="primary" type="grow" />
+            <Spinner variant="primary" animation="grow" />
           </div>
         );
       }
@@ -271,7 +271,7 @@ export default function TimelinePageTemplateUI<TManageItems>(
       body = (
         <>
           <div
-            className="fixed-top mt-appbar info-card-container"
+            className="info-card-container"
             data-collapse={infoCardCollapse ? "true" : "false"}
           >
             <Svg
@@ -304,7 +304,7 @@ export default function TimelinePageTemplateUI<TManageItems>(
     } else {
       body = (
         <div className="full-viewport-center-child">
-          <Spinner color="primary" type="grow" />
+          <Spinner variant="primary" animation="grow" />
         </div>
       );
     }
@@ -313,13 +313,7 @@ export default function TimelinePageTemplateUI<TManageItems>(
   return (
     <>
       <AppBar />
-      <div>
-        <div
-          style={{ height: 56 + cardHeight }}
-          className="timeline-page-top-space flex-fix-length"
-        />
-        {body}
-      </div>
+      {body}
     </>
   );
 }
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx
index 6a8bb000..42f83b52 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx
@@ -1,7 +1,7 @@
 import React from "react";
-import { Button, Spinner, Row, Col } from "reactstrap";
 import { useTranslation } from "react-i18next";
 import Svg from "react-inlinesvg";
+import { Button, Spinner, Row, Col, Form } from "react-bootstrap";
 import textIcon from "bootstrap-icons/icons/card-text.svg";
 import imageIcon from "bootstrap-icons/icons/image.svg";
 
@@ -10,8 +10,6 @@ import { UiLogicError } from "@/common";
 import { pushAlert } from "@/services/alert";
 import { TimelineCreatePostRequest } from "@/services/timeline";
 
-import FileInput from "../common/FileInput";
-
 interface TimelinePostEditImageProps {
   onSelect: (blob: Blob | null) => void;
 }
@@ -59,11 +57,11 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => {
 
   return (
     <>
-      <FileInput
-        labelText={t("chooseImage")}
+      <Form.File
+        label={t("chooseImage")}
         onChange={onInputChange}
         accept="image/*"
-        className="mx-3 my-1"
+        className="mx-3 my-1 d-inline-block"
       />
       {fileUrl && error == null && (
         <img
@@ -189,7 +187,8 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
       <Row>
         <Col className="px-1 py-1">
           {kind === "text" ? (
-            <textarea
+            <Form.Control
+              as="textarea"
               className="w-100 h-100 timeline-post-edit"
               value={text}
               disabled={state === "process"}
@@ -203,7 +202,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
             <TimelinePostEditImage onSelect={onImageSelect} />
           )}
         </Col>
-        <Col sm="col-auto align-self-end m-1">
+        <Col xs="auto" className="align-self-end m-1">
           {(() => {
             if (state === "input") {
               return (
@@ -216,13 +215,17 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
                       onClick={toggleKind}
                     />
                   </div>
-                  <Button color="primary" onClick={onSend} disabled={!canSend}>
+                  <Button
+                    variant="primary"
+                    onClick={onSend}
+                    disabled={!canSend}
+                  >
                     {t("timeline.send")}
                   </Button>
                 </>
               );
             } else {
-              return <Spinner />;
+              return <Spinner variant="primary" animation="border" />;
             }
           })()}
         </Col>
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass
index 960c992d..1862de02 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass
+++ b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass
@@ -119,14 +119,6 @@ $timeline-line-color-current: #36c2e6
   background: change-color($color: white, $alpha: 0.8)
   z-index: 100
 
-textarea.timeline-post-edit
-  @extend .border-primary
-  @extend .rounded
-
-  &:focus
-    outline: none
-    box-shadow: 0 0 5px 0 $primary
-
 .timeline-page-top-space
   transition: height 0.5s
 
@@ -147,3 +139,20 @@ textarea.timeline-post-edit
   border-radius: 50%
   vertical-align: middle
   margin-right: 0.6em
+
+.info-card-container
+  position: sticky
+  z-index: 1
+
+  .info-card-collapse-button
+    z-index: 1
+    position: relative
+
+  .info-card-content
+    width: 100%
+    transform-origin: right top
+    transition: transform 0.5s
+
+  &[data-collapse='true']
+    .info-card-content
+      transform: scale(0)
diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx
index e3e89057..bf5c3105 100644
--- a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx
@@ -1,14 +1,8 @@
 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 { Dropdown, Button } from "react-bootstrap";
 
 import { useAvatar } from "@/services/user";
 import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline";
@@ -23,7 +17,7 @@ export type TimelineInfoCardProps = TimelineCardComponentProps<
 >;
 
 const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => {
-  const { onHeight, onManage } = props;
+  const { onHeight, onMember, onManage } = props;
 
   const { t } = useTranslation();
 
@@ -43,18 +37,10 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => {
     return () => subscription.unsubscribe();
   });
 
-  const [manageDropdownOpen, setManageDropdownOpen] = React.useState<boolean>(
-    false
-  );
-  const toggleManageDropdown = React.useCallback(
-    (): void => setManageDropdownOpen((old) => !old),
-    []
-  );
-
   return (
     <div
       ref={containerRef}
-      className={clsx("rounded border p-2 bg-light", props.className)}
+      className={clsx("rounded border p-2 bg-light clearfix", props.className)}
       onTransitionEnd={notifyHeight}
     >
       <h3 className="text-primary mx-3 d-inline-block align-middle">
@@ -77,28 +63,28 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => {
       </small>
       <div className="text-right mt-2">
         {onManage != null ? (
-          <Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}>
-            <DropdownToggle outline color="primary">
+          <Dropdown>
+            <Dropdown.Toggle variant="outline-primary">
               {t("timeline.manage")}
-            </DropdownToggle>
-            <DropdownMenu>
-              <DropdownItem onClick={() => onManage("property")}>
+            </Dropdown.Toggle>
+            <Dropdown.Menu>
+              <Dropdown.Item onClick={() => onManage("property")}>
                 {t("timeline.manageItem.property")}
-              </DropdownItem>
-              <DropdownItem onClick={props.onMember}>
+              </Dropdown.Item>
+              <Dropdown.Item onClick={onMember}>
                 {t("timeline.manageItem.member")}
-              </DropdownItem>
-              <DropdownItem divider />
-              <DropdownItem
+              </Dropdown.Item>
+              <Dropdown.Divider />
+              <Dropdown.Item
                 className="text-danger"
                 onClick={() => onManage("delete")}
               >
                 {t("timeline.manageItem.delete")}
-              </DropdownItem>
-            </DropdownMenu>
+              </Dropdown.Item>
+            </Dropdown.Menu>
           </Dropdown>
         ) : (
-          <Button color="primary" outline onClick={props.onMember}>
+          <Button variant="outline-primary" onClick={onMember}>
             {t("timeline.memberButton")}
           </Button>
         )}
diff --git a/Timeline/ClientApp/src/app/views/timeline/timeline.sass b/Timeline/ClientApp/src/app/views/timeline/timeline.sass
index 0eeec73a..e69de29b 100644
--- a/Timeline/ClientApp/src/app/views/timeline/timeline.sass
+++ b/Timeline/ClientApp/src/app/views/timeline/timeline.sass
@@ -1,14 +0,0 @@
-.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)
diff --git a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx
index 1dd2ee8b..ffa2218b 100644
--- a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx
+++ b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx
@@ -1,14 +1,7 @@
 import React, { useState, useEffect } from "react";
 import { useTranslation } from "react-i18next";
-import {
-  Modal,
-  ModalHeader,
-  Row,
-  Button,
-  ModalBody,
-  ModalFooter,
-} from "reactstrap";
 import { AxiosError } from "axios";
+import { Modal, Row, Button } from "react-bootstrap";
 
 import { UiLogicError } from "@/common";
 
@@ -56,7 +49,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
 
   const closeDialog = props.close;
 
-  const toggle = React.useCallback((): void => {
+  const close = React.useCallback((): void => {
     if (!(state === "uploading")) {
       closeDialog();
     }
@@ -163,23 +156,25 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
   };
 
   return (
-    <Modal isOpen={props.open} toggle={toggle}>
-      <ModalHeader> {t("userPage.dialogChangeAvatar.title")}</ModalHeader>
+    <Modal show={props.open} onHide={close}>
+      <Modal.Header>
+        <Modal.Title> {t("userPage.dialogChangeAvatar.title")}</Modal.Title>
+      </Modal.Header>
       {(() => {
         if (state === "select") {
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 <Row>{t("userPage.dialogChangeAvatar.prompt.select")}</Row>
                 <Row>
                   <input type="file" accept="image/*" onChange={onSelectFile} />
                 </Row>
-              </ModalBody>
-              <ModalFooter>
-                <Button color="secondary" onClick={toggle}>
+              </Modal.Body>
+              <Modal.Footer>
+                <Button variant="secondary" onClick={close}>
                   {t("operationDialog.cancel")}
                 </Button>
-              </ModalFooter>
+              </Modal.Footer>
             </>
           );
         } else if (state === "crop") {
@@ -188,7 +183,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
           }
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 <Row className="justify-content-center">
                   <ImageCropper
                     clip={clip}
@@ -198,12 +193,12 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
                   />
                 </Row>
                 <Row>{t("userPage.dialogChangeAvatar.prompt.crop")}</Row>
-              </ModalBody>
-              <ModalFooter>
-                <Button color="secondary" onClick={toggle}>
+              </Modal.Body>
+              <Modal.Footer>
+                <Button variant="secondary" onClick={close}>
                   {t("operationDialog.cancel")}
                 </Button>
-                <Button color="secondary" onClick={onCropPrevious}>
+                <Button variant="secondary" onClick={onCropPrevious}>
                   {t("operationDialog.previousStep")}
                 </Button>
                 <Button
@@ -215,87 +210,87 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
                 >
                   {t("operationDialog.nextStep")}
                 </Button>
-              </ModalFooter>
+              </Modal.Footer>
             </>
           );
         } else if (state === "processcrop") {
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 <Row>
                   {t("userPage.dialogChangeAvatar.prompt.processingCrop")}
                 </Row>
-              </ModalBody>
-              <ModalFooter>
-                <Button color="secondary" onClick={toggle}>
+              </Modal.Body>
+              <Modal.Footer>
+                <Button variant="secondary" onClick={close}>
                   {t("operationDialog.cancel")}
                 </Button>
-                <Button color="secondary" onClick={onPreviewPrevious}>
+                <Button variant="secondary" onClick={onPreviewPrevious}>
                   {t("operationDialog.previousStep")}
                 </Button>
-              </ModalFooter>
+              </Modal.Footer>
             </>
           );
         } else if (state === "preview") {
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 {createPreviewRow()}
                 <Row>{t("userPage.dialogChangeAvatar.prompt.preview")}</Row>
-              </ModalBody>
-              <ModalFooter>
-                <Button color="secondary" onClick={toggle}>
+              </Modal.Body>
+              <Modal.Footer>
+                <Button variant="secondary" onClick={close}>
                   {t("operationDialog.cancel")}
                 </Button>
-                <Button color="secondary" onClick={onPreviewPrevious}>
+                <Button variant="secondary" onClick={onPreviewPrevious}>
                   {t("operationDialog.previousStep")}
                 </Button>
-                <Button color="primary" onClick={upload}>
+                <Button variant="primary" onClick={upload}>
                   {t("userPage.dialogChangeAvatar.upload")}
                 </Button>
-              </ModalFooter>
+              </Modal.Footer>
             </>
           );
         } else if (state === "uploading") {
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 {createPreviewRow()}
                 <Row>{t("userPage.dialogChangeAvatar.prompt.uploading")}</Row>
-              </ModalBody>
-              <ModalFooter></ModalFooter>
+              </Modal.Body>
+              <Modal.Footer></Modal.Footer>
             </>
           );
         } else if (state === "success") {
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 <Row className="p-4 text-success">
                   {t("operationDialog.success")}
                 </Row>
-              </ModalBody>
-              <ModalFooter>
-                <Button color="success" onClick={toggle}>
+              </Modal.Body>
+              <Modal.Footer>
+                <Button variant="success" onClick={close}>
                   {t("operationDialog.ok")}
                 </Button>
-              </ModalFooter>
+              </Modal.Footer>
             </>
           );
         } else {
           return (
             <>
-              <ModalBody className="container">
+              <Modal.Body className="container">
                 {createPreviewRow()}
                 <Row className="text-danger">{trueMessage}</Row>
-              </ModalBody>
-              <ModalFooter>
-                <Button color="secondary" onClick={toggle}>
+              </Modal.Body>
+              <Modal.Footer>
+                <Button variant="secondary" onClick={close}>
                   {t("operationDialog.cancel")}
                 </Button>
-                <Button color="primary" onClick={upload}>
+                <Button variant="primary" onClick={upload}>
                   {t("operationDialog.retry")}
                 </Button>
-              </ModalFooter>
+              </Modal.Footer>
             </>
           );
         }
diff --git a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx
index 1a111877..f1878b5c 100644
--- a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx
+++ b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx
@@ -1,14 +1,8 @@
 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 { Dropdown, Button } from "react-bootstrap";
 
 import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline";
 import { useAvatar } from "@/services/user";
@@ -42,24 +36,16 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => {
     return () => subscription.unsubscribe();
   });
 
-  const [manageDropdownOpen, setManageDropdownOpen] = React.useState<boolean>(
-    false
-  );
-  const toggleManageDropdown = React.useCallback(
-    (): void => setManageDropdownOpen((old) => !old),
-    []
-  );
-
   return (
     <div
       ref={containerRef}
-      className={clsx("rounded border bg-light p-2", props.className)}
+      className={clsx("rounded border bg-light p-2 clearfix", props.className)}
       onTransitionEnd={notifyHeight}
     >
       <BlobImage
         blob={avatar}
         onLoad={notifyHeight}
-        className="avatar large mr-2 mb-2 rounded-circle float-left"
+        className="avatar large mr-2 rounded-circle float-left"
       />
       <div>
         {props.timeline.owner.nickname}
@@ -73,27 +59,27 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => {
       </small>
       <div className="text-right mt-2">
         {onManage != null ? (
-          <Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}>
-            <DropdownToggle outline color="primary">
+          <Dropdown>
+            <Dropdown.Toggle variant="outline-primary">
               {t("timeline.manage")}
-            </DropdownToggle>
-            <DropdownMenu>
-              <DropdownItem onClick={() => onManage("nickname")}>
+            </Dropdown.Toggle>
+            <Dropdown.Menu>
+              <Dropdown.Item onClick={() => onManage("nickname")}>
                 {t("timeline.manageItem.nickname")}
-              </DropdownItem>
-              <DropdownItem onClick={() => onManage("avatar")}>
+              </Dropdown.Item>
+              <Dropdown.Item onClick={() => onManage("avatar")}>
                 {t("timeline.manageItem.avatar")}
-              </DropdownItem>
-              <DropdownItem onClick={() => onManage("property")}>
+              </Dropdown.Item>
+              <Dropdown.Item onClick={() => onManage("property")}>
                 {t("timeline.manageItem.property")}
-              </DropdownItem>
-              <DropdownItem onClick={props.onMember}>
+              </Dropdown.Item>
+              <Dropdown.Item onClick={props.onMember}>
                 {t("timeline.manageItem.member")}
-              </DropdownItem>
-            </DropdownMenu>
+              </Dropdown.Item>
+            </Dropdown.Menu>
           </Dropdown>
         ) : (
-          <Button color="primary" outline onClick={props.onMember}>
+          <Button variant="outline-primary" onClick={props.onMember}>
             {t("timeline.memberButton")}
           </Button>
         )}
diff --git a/Timeline/ClientApp/yarn.lock b/Timeline/ClientApp/yarn.lock
index b60c6fef..8ca54343 100644
--- a/Timeline/ClientApp/yarn.lock
+++ b/Timeline/ClientApp/yarn.lock
@@ -1488,7 +1488,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4":
+"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4":
   version: 7.10.5
   resolution: "@babel/runtime@npm:7.10.5::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.10.5.tgz"
   dependencies:
@@ -2015,16 +2015,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/reactstrap@npm:^8.5.1":
-  version: 8.5.1
-  resolution: "@types/reactstrap@npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freactstrap%2Fdownload%2F%40types%2Freactstrap-8.5.1.tgz"
-  dependencies:
-    "@types/react": "*"
-    popper.js: ^1.14.1
-  checksum: 87b383af721243214a3ae8bf4867f932ed0fcd56489337290ce090a264e3db6419ee266d57dcdc406bdbf9185c5cf2c742402e870c396c6735f8ebc00c06af09
-  languageName: node
-  linkType: hard
-
 "@types/resolve@npm:0.0.8":
   version: 0.0.8
   resolution: "@types/resolve@npm:0.0.8::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz"
@@ -3847,7 +3837,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"classnames@npm:^2.2.3, classnames@npm:^2.2.6":
+"classnames@npm:^2.2.6":
   version: 2.2.6
   resolution: "classnames@npm:2.2.6::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fclassnames%2Fdownload%2Fclassnames-2.2.6.tgz"
   checksum: 490eaeca5931846737ffd33e472a701d268d5b8bc5717dd4cf108a127b06e86e05350e06799abbbe763a0e4c945b4217f6700b7ae00ddc703505682c370e5cf2
@@ -4288,19 +4278,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"create-react-context@npm:^0.3.0":
-  version: 0.3.0
-  resolution: "create-react-context@npm:0.3.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fcreate-react-context%2Fdownload%2Fcreate-react-context-0.3.0.tgz"
-  dependencies:
-    gud: ^1.0.0
-    warning: ^4.0.3
-  peerDependencies:
-    prop-types: ^15.0.0
-    react: ^0.14.0 || ^15.0.0 || ^16.0.0
-  checksum: dfb153bbcfe44fb0b5d74e097f41fef382c9ea513084f604fea1759aea6683da0eef13b02ede2f0672c4eeacdd8efce0023935ca02909fcb2ae8be28b9aa63f5
-  languageName: node
-  linkType: hard
-
 "cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.2":
   version: 7.0.3
   resolution: "cross-spawn@npm:7.0.3::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fcross-spawn%2Fdownload%2Fcross-spawn-7.0.3.tgz"
@@ -4559,7 +4536,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"deep-equal@npm:^1.0.1, deep-equal@npm:^1.1.1":
+"deep-equal@npm:^1.0.1":
   version: 1.1.1
   resolution: "deep-equal@npm:1.1.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeep-equal%2Fdownload%2Fdeep-equal-1.1.1.tgz"
   dependencies:
@@ -4798,15 +4775,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"dom-helpers@npm:^3.4.0":
-  version: 3.4.0
-  resolution: "dom-helpers@npm:3.4.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-helpers%2Fdownload%2Fdom-helpers-3.4.0.tgz"
-  dependencies:
-    "@babel/runtime": ^7.1.2
-  checksum: 7625598dbfef65a50801e194b88f277ab398dd92c27398280fd8d69294326f05d9b0588922560ba50e10d9fe5ee8096f90995d679b7db2b776afd579d495b962
-  languageName: node
-  linkType: hard
-
 "dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.1.0, dom-helpers@npm:^5.1.2":
   version: 5.2.0
   resolution: "dom-helpers@npm:5.2.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-helpers%2Fdownload%2Fdom-helpers-5.2.0.tgz"
@@ -6248,13 +6216,6 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"gud@npm:^1.0.0":
-  version: 1.0.0
-  resolution: "gud@npm:1.0.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fgud%2Fdownload%2Fgud-1.0.0.tgz"
-  checksum: 08be6bf30eb713b0e115a4676418b3805b739703956fd861710f59ce355c047102954e4d79172b180912d06794e8d94f702e9367ac6843c2fae40c8c726a4907
-  languageName: node
-  linkType: hard
-
 "handle-thing@npm:^2.0.0":
   version: 2.0.1
   resolution: "handle-thing@npm:2.0.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fhandle-thing%2Fdownload%2Fhandle-thing-2.0.1.tgz"
@@ -9068,13 +9029,6 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"popper.js@npm:^1.14.1, popper.js@npm:^1.14.4":
-  version: 1.16.1
-  resolution: "popper.js@npm:1.16.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fpopper.js%2Fdownload%2Fpopper.js-1.16.1.tgz"
-  checksum: eb53806fb7680e31c7d1db096f95438a40a7cb869f7ee0e27100c0860f11583a0d322606c6073618e7fe8865f834ec36482901b547256d9a0cf2b22434bca75d
-  languageName: node
-  linkType: hard
-
 "portfinder@npm:^1.0.25, portfinder@npm:^1.0.26":
   version: 1.0.27
   resolution: "portfinder@npm:1.0.27::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fportfinder%2Fdownload%2Fportfinder-1.0.27.tgz"
@@ -9651,7 +9605,7 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"prop-types@npm:^15.5.10, prop-types@npm:^15.5.8, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2":
+"prop-types@npm:^15.5.10, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2":
   version: 15.7.2
   resolution: "prop-types@npm:15.7.2::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fprop-types%2Fdownload%2Fprop-types-15.7.2.tgz"
   dependencies:
@@ -9982,23 +9936,6 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"react-popper@npm:^1.3.6":
-  version: 1.3.7
-  resolution: "react-popper@npm:1.3.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-popper%2Fdownload%2Freact-popper-1.3.7.tgz"
-  dependencies:
-    "@babel/runtime": ^7.1.2
-    create-react-context: ^0.3.0
-    deep-equal: ^1.1.1
-    popper.js: ^1.14.4
-    prop-types: ^15.6.1
-    typed-styles: ^0.0.7
-    warning: ^4.0.2
-  peerDependencies:
-    react: 0.14.x || ^15.0.0 || ^16.0.0
-  checksum: 09ef58054bd9d449283939af861cbb205aee771289884fa62e30a6927163fab53e865593c0e014364d807a8e219d9642be6d34ae58b1e4f2f0530ffc9b289e5a
-  languageName: node
-  linkType: hard
-
 "react-responsive@npm:^8.1.0":
   version: 8.1.0
   resolution: "react-responsive@npm:8.1.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-responsive%2Fdownload%2Freact-responsive-8.1.0.tgz"
@@ -10062,21 +9999,6 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"react-transition-group@npm:^2.3.1":
-  version: 2.9.0
-  resolution: "react-transition-group@npm:2.9.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-transition-group%2Fdownload%2Freact-transition-group-2.9.0.tgz"
-  dependencies:
-    dom-helpers: ^3.4.0
-    loose-envify: ^1.4.0
-    prop-types: ^15.6.2
-    react-lifecycles-compat: ^3.0.4
-  peerDependencies:
-    react: ">=15.0.0"
-    react-dom: ">=15.0.0"
-  checksum: eefed08c48a0d377b5fac3ac7fac54ed986d5dcdd4494f36075954c9d2d257072a9d18cc02c6d8e6698034722b1ff9419ad0c24ef068dea742edcff9f8ea4ba9
-  languageName: node
-  linkType: hard
-
 "react-transition-group@npm:^4.4.1":
   version: 4.4.1
   resolution: "react-transition-group@npm:4.4.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-transition-group%2Fdownload%2Freact-transition-group-4.4.1.tgz"
@@ -10103,22 +10025,6 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"reactstrap@npm:^8.5.1":
-  version: 8.5.1
-  resolution: "reactstrap@npm:8.5.1::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Freactstrap%2Fdownload%2Freactstrap-8.5.1.tgz"
-  dependencies:
-    "@babel/runtime": ^7.2.0
-    classnames: ^2.2.3
-    prop-types: ^15.5.8
-    react-popper: ^1.3.6
-    react-transition-group: ^2.3.1
-  peerDependencies:
-    react: ^16.3.0
-    react-dom: ^16.3.0
-  checksum: afa5cba1a9ebff336ec4b7e8970bbffef2070cf7925e761a43cd499d441877866f181d46197545b9a55483809811f999efe1dd4628c3cd304754607833ba802e
-  languageName: node
-  linkType: hard
-
 "read-pkg-up@npm:^2.0.0":
   version: 2.0.0
   resolution: "read-pkg-up@npm:2.0.0::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fread-pkg-up%2Fdownload%2Fread-pkg-up-2.0.0.tgz"
@@ -11657,7 +11563,6 @@ fsevents@~2.1.2:
     "@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
@@ -11705,7 +11610,6 @@ fsevents@~2.1.2:
     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
     sass: ^1.26.10
@@ -11948,13 +11852,6 @@ fsevents@~2.1.2:
   languageName: node
   linkType: hard
 
-"typed-styles@npm:^0.0.7":
-  version: 0.0.7
-  resolution: "typed-styles@npm:0.0.7::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Ftyped-styles%2Fdownload%2Ftyped-styles-0.0.7.tgz"
-  checksum: 4988d78dafc45bc0ad73393f767c7958aabe121a9310d5c864ad6e064fd8bc049ac9a16ca2b411640e94cc95f8af3a67c78771f5eab5c211fcb60ecb71fdd2b9
-  languageName: node
-  linkType: hard
-
 "typedarray@npm:^0.0.6":
   version: 0.0.6
   resolution: "typedarray@npm:0.0.6::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypedarray%2Fdownload%2Ftypedarray-0.0.6.tgz"
@@ -12301,7 +12198,7 @@ typescript@^4.0.2:
   languageName: node
   linkType: hard
 
-"warning@npm:^4.0.0, warning@npm:^4.0.2, warning@npm:^4.0.3":
+"warning@npm:^4.0.0, warning@npm:^4.0.3":
   version: 4.0.3
   resolution: "warning@npm:4.0.3::__archiveUrl=https%3A%2F%2Fregistry.npm.taobao.org%2Fwarning%2Fdownload%2Fwarning-4.0.3.tgz"
   dependencies:
-- 
cgit v1.2.3

