From 29da3cef05ef138af2028ad44a27d706e9ae2df8 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Jun 2021 18:46:58 +0800
Subject: ...

---
 FrontEnd/src/views/common/button/LoadingButton.tsx | 28 ++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 FrontEnd/src/views/common/button/LoadingButton.tsx

(limited to 'FrontEnd/src/views/common/button/LoadingButton.tsx')

diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
new file mode 100644
index 00000000..fd1c19b3
--- /dev/null
+++ b/FrontEnd/src/views/common/button/LoadingButton.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+
+const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({
+  loading,
+  variant,
+  disabled,
+  ...otherProps
+}) => {
+  return (
+    <Button
+      variant={variant != null ? `outline-${variant}` : "outline-primary"}
+      disabled={disabled || loading}
+      {...otherProps}
+    >
+      {otherProps.children}
+      {loading ? (
+        <Spinner
+          className="ms-1"
+          variant={variant}
+          animation="grow"
+          size="sm"
+        />
+      ) : null}
+    </Button>
+  );
+};
+
+export default LoadingButton;
-- 
cgit v1.2.3


From a168336c0761b263ee5371218cbf6da236c0acce Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Jun 2021 19:13:19 +0800
Subject: ...

---
 FrontEnd/src/views/admin/UserAdmin.tsx             |   9 +-
 FrontEnd/src/views/common/Menu.tsx                 |  26 ++---
 FrontEnd/src/views/common/SearchInput.tsx          |  13 +--
 FrontEnd/src/views/common/Spinner.tsx              |  13 +++
 FrontEnd/src/views/common/TabPages.tsx             |  20 +---
 FrontEnd/src/views/common/alert/AlertHost.tsx      |  11 +-
 FrontEnd/src/views/common/button/LoadingButton.tsx |  23 ++---
 .../src/views/common/dailog/OperationDialog.tsx    |  71 ++++++-------
 FrontEnd/src/views/login/index.tsx                 | 114 ++++++++++-----------
 .../src/views/timeline-common/MarkdownPostEdit.tsx |   8 +-
 .../src/views/timeline-common/TimelineMember.tsx   |  21 ++--
 .../views/timeline-common/TimelinePageTemplate.tsx |   1 -
 .../TimelinePostDeleteConfirmDialog.tsx            |  40 --------
 .../src/views/timeline-common/TimelinePostEdit.tsx |   1 -
 .../src/views/timeline-common/TimelinePostView.tsx |   6 +-
 15 files changed, 142 insertions(+), 235 deletions(-)
 create mode 100644 FrontEnd/src/views/common/Spinner.tsx
 delete mode 100644 FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx

(limited to 'FrontEnd/src/views/common/button/LoadingButton.tsx')

diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx
index 125219c3..481db1cc 100644
--- a/FrontEnd/src/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/views/admin/UserAdmin.tsx
@@ -15,6 +15,7 @@ import {
 import { Trans, useTranslation } from "react-i18next";
 import Button from "../common/button/Button";
 import TextButton from "../common/button/TextButton";
+import Spinner from "../common/Spinner";
 
 interface DialogProps<TData = undefined, TReturn = undefined> {
   open: boolean;
@@ -203,7 +204,7 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
   const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false);
 
   return (
-    <ListGroup.Item className="admin-user-item">
+    <div className="admin-user-item">
       <i
         className="bi-pencil-square float-end icon-button text-warning"
         onClick={() => setEditMaskVisible(true)}
@@ -242,7 +243,7 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
           onClick={on[kDelete]}
         />
       </div>
-    </ListGroup.Item>
+    </div>
   );
 };
 
@@ -251,8 +252,6 @@ interface UserAdminProps {
 }
 
 const UserAdmin: React.FC<UserAdminProps> = () => {
-  const { t } = useTranslation();
-
   type DialogInfo =
     | null
     | {
@@ -390,7 +389,7 @@ const UserAdmin: React.FC<UserAdminProps> = () => {
       </>
     );
   } else {
-    return <Spinner animation="border" />;
+    return <Spinner />;
   }
 };
 
diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/Menu.tsx
index ae73a331..a5d2ec2c 100644
--- a/FrontEnd/src/views/common/Menu.tsx
+++ b/FrontEnd/src/views/common/Menu.tsx
@@ -1,9 +1,9 @@
 import React from "react";
 import classnames from "classnames";
-import { OverlayTrigger, OverlayTriggerProps, Popover } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 
-import { BootstrapThemeColor, convertI18nText, I18nText } from "@/common";
+import { convertI18nText, I18nText } from "@/common";
+import { PaletteColorType } from "@/palette";
 
 export type MenuItem =
   | {
@@ -13,7 +13,7 @@ export type MenuItem =
       type: "button";
       text: I18nText;
       iconClassName?: string;
-      color?: BootstrapThemeColor;
+      color?: PaletteColorType;
       onClick: () => void;
     };
 
@@ -67,26 +67,14 @@ export default Menu;
 
 export interface PopupMenuProps {
   items: MenuItems;
-  children: OverlayTriggerProps["children"];
+  children: React.ReactElement;
 }
 
 export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => {
   const [show, setShow] = React.useState<boolean>(false);
   const toggle = (): void => setShow(!show);
 
-  return (
-    <OverlayTrigger
-      trigger="click"
-      rootClose
-      overlay={
-        <Popover id="menu-popover">
-          <Menu items={items} onItemClicked={() => setShow(false)} />
-        </Popover>
-      }
-      show={show}
-      onToggle={toggle}
-    >
-      {children}
-    </OverlayTrigger>
-  );
+  // TODO:
+
+  return <Menu items={items} onItemClicked={() => setShow(false)} />;
 };
diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx
index 79eb2732..fff11b95 100644
--- a/FrontEnd/src/views/common/SearchInput.tsx
+++ b/FrontEnd/src/views/common/SearchInput.tsx
@@ -1,7 +1,8 @@
 import React, { useCallback } from "react";
 import classnames from "classnames";
 import { useTranslation } from "react-i18next";
-import { Spinner, Form, Button } from "react-bootstrap";
+
+import LoadingButton from "./button/LoadingButton";
 
 export interface SearchInputProps {
   value: string;
@@ -64,13 +65,9 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
           "flex-shrink-0"
         )}
       >
-        {props.loading ? (
-          <Spinner variant="primary" animation="border" />
-        ) : (
-          <Button variant="outline-primary" onClick={props.onButtonClick}>
-            {props.buttonText ?? t("search")}
-          </Button>
-        )}
+        <LoadingButton loading={props.loading} onClick={props.onButtonClick}>
+          {props.buttonText ?? t("search")}
+        </LoadingButton>
       </div>
     </div>
   );
diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx
new file mode 100644
index 00000000..783c9be2
--- /dev/null
+++ b/FrontEnd/src/views/common/Spinner.tsx
@@ -0,0 +1,13 @@
+import { PaletteColorType } from "@/palette";
+import React from "react";
+
+export interface SpinnerProps {
+  size?: "sm" | "md" | "lg" | number;
+  color?: PaletteColorType;
+}
+
+export default function Spinner(
+  props: SpinnerProps
+): React.ReactElement | null {
+  return <span />;
+}
diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/TabPages.tsx
index 2b1d91cb..b7a9fb36 100644
--- a/FrontEnd/src/views/common/TabPages.tsx
+++ b/FrontEnd/src/views/common/TabPages.tsx
@@ -1,5 +1,4 @@
 import React from "react";
-import { Nav } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 
 import { convertI18nText, I18nText, UiLogicError } from "@/common";
@@ -31,6 +30,8 @@ const TabPages: React.FC<TabPagesProps> = ({
   pageContainerClassName,
   pageContainerStyle,
 }) => {
+  // TODO:
+
   if (pages.length === 0) {
     throw new UiLogicError("Page list can't be empty.");
   }
@@ -47,23 +48,6 @@ const TabPages: React.FC<TabPagesProps> = ({
 
   return (
     <div className={className} style={style}>
-      <Nav variant="tabs" className={navClassName} style={navStyle}>
-        {pages.map((page) => (
-          <Nav.Item key={page.id}>
-            <Nav.Link
-              active={tab === page.id}
-              onClick={() => {
-                setTab(page.id);
-              }}
-            >
-              {convertI18nText(page.tabText, t)}
-            </Nav.Link>
-          </Nav.Item>
-        ))}
-        {actions != null && (
-          <div className="ms-auto cru-tab-pages-action-area">{actions}</div>
-        )}
-      </Nav>
       <div className={pageContainerClassName} style={pageContainerStyle}>
         {currentPage.page}
       </div>
diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx
index 949be7ed..21b9882d 100644
--- a/FrontEnd/src/views/common/alert/AlertHost.tsx
+++ b/FrontEnd/src/views/common/alert/AlertHost.tsx
@@ -1,7 +1,6 @@
 import React from "react";
 import without from "lodash/without";
 import { useTranslation } from "react-i18next";
-import { Alert } from "react-bootstrap";
 
 import {
   alertService,
@@ -52,13 +51,7 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
   };
 
   return (
-    <Alert
-      className="m-3"
-      variant={alert.type ?? "primary"}
-      onClick={cancelTimer}
-      onClose={close}
-      dismissible
-    >
+    <div className="m-3" onClick={cancelTimer}>
       {(() => {
         const { message } = alert;
         if (typeof message === "function") {
@@ -66,7 +59,7 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
           return <Message />;
         } else return convertI18nText(message, t);
       })()}
-    </Alert>
+    </div>
   );
 };
 
diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
index fd1c19b3..aee83aa2 100644
--- a/FrontEnd/src/views/common/button/LoadingButton.tsx
+++ b/FrontEnd/src/views/common/button/LoadingButton.tsx
@@ -1,26 +1,19 @@
 import React from "react";
 
-const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({
+import { CommonButtonProps } from "./common";
+import Button from "./Button";
+import Spinner from "../Spinner";
+
+const LoadingButton: React.FC<{ loading?: boolean } & CommonButtonProps> = ({
   loading,
-  variant,
   disabled,
+  color,
   ...otherProps
 }) => {
   return (
-    <Button
-      variant={variant != null ? `outline-${variant}` : "outline-primary"}
-      disabled={disabled || loading}
-      {...otherProps}
-    >
+    <Button color={color} disabled={disabled || loading} {...otherProps}>
       {otherProps.children}
-      {loading ? (
-        <Spinner
-          className="ms-1"
-          variant={variant}
-          animation="grow"
-          size="sm"
-        />
-      ) : null}
+      {loading ? <Spinner color={color} /> : null}
     </Button>
   );
 };
diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx
index 129e85d5..1e765276 100644
--- a/FrontEnd/src/views/common/dailog/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx
@@ -5,6 +5,8 @@ import moment from "moment";
 
 import { convertI18nText, I18nText, UiLogicError } from "@/common";
 
+import { PaletteColorType } from "@/palette";
+
 import Button from "../button/Button";
 import LoadingButton from "../button/LoadingButton";
 import Dialog from "./Dialog";
@@ -144,7 +146,7 @@ export interface OperationDialogProps<
   open: boolean;
   onClose: () => void;
   title: I18nText | (() => React.ReactNode);
-  themeColor?: "danger" | "success" | string;
+  themeColor?: PaletteColorType;
   onProcess: (
     inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
   ) => Promise<TData>;
@@ -290,50 +292,42 @@ const OperationDialog = <
 
             if (item.type === "text") {
               return (
-                <Form.Group key={index}>
+                <div key={index}>
                   {item.label && (
-                    <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
+                    <label>{convertI18nText(item.label, t)}</label>
                   )}
-                  <Form.Control
+                  <input
                     type={item.password === true ? "password" : "text"}
                     value={value as string}
                     onChange={(e) => {
                       const v = e.target.value;
                       updateValue(index, v);
                     }}
-                    isInvalid={error != null}
                     disabled={process}
                   />
-                  {error != null && (
-                    <Form.Control.Feedback type="invalid">
-                      {error}
-                    </Form.Control.Feedback>
-                  )}
-                  {item.helperText && (
-                    <Form.Text>{t(item.helperText)}</Form.Text>
-                  )}
-                </Form.Group>
+                  {error != null && <div>{error}</div>}
+                  {item.helperText && <div>{t(item.helperText)}</div>}
+                </div>
               );
             } else if (item.type === "bool") {
               return (
-                <Form.Group key={index}>
-                  <Form.Check<"input">
+                <div key={index}>
+                  <input
                     type="checkbox"
                     checked={value as boolean}
                     onChange={(event) => {
                       updateValue(index, event.currentTarget.checked);
                     }}
-                    label={convertI18nText(item.label, t)}
                     disabled={process}
                   />
-                </Form.Group>
+                  <label>{convertI18nText(item.label, t)}</label>
+                </div>
               );
             } else if (item.type === "select") {
               return (
-                <Form.Group key={index}>
-                  <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
-                  <Form.Control
-                    as="select"
+                <div key={index}>
+                  <label>{convertI18nText(item.label, t)}</label>
+                  <select
                     value={value as string}
                     onChange={(event) => {
                       updateValue(index, event.target.value);
@@ -348,14 +342,14 @@ const OperationDialog = <
                         </option>
                       );
                     })}
-                  </Form.Control>
-                </Form.Group>
+                  </select>
+                </div>
               );
             } else if (item.type === "color") {
               return (
-                <Form.Group key={index}>
+                <div key={index}>
                   {item.canBeNull ? (
-                    <Form.Check<"input">
+                    <input
                       type="checkbox"
                       checked={value !== null}
                       onChange={(event) => {
@@ -365,42 +359,35 @@ const OperationDialog = <
                           updateValue(index, null);
                         }
                       }}
-                      label={convertI18nText(item.label, t)}
                       disabled={process}
                     />
-                  ) : (
-                    <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
-                  )}
+                  ) : null}
+                  <label>{convertI18nText(item.label, t)}</label>
                   {value !== null && (
                     <TwitterPicker
                       color={value as string}
                       onChange={(result) => updateValue(index, result.hex)}
                     />
                   )}
-                </Form.Group>
+                </div>
               );
             } else if (item.type === "datetime") {
               return (
-                <Form.Group key={index}>
+                <div key={index}>
                   {item.label && (
-                    <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
+                    <label>{convertI18nText(item.label, t)}</label>
                   )}
-                  <Form.Control
+                  <input
                     type="datetime-local"
                     value={value as string}
                     onChange={(e) => {
                       const v = e.target.value;
                       updateValue(index, v);
                     }}
-                    isInvalid={error != null}
                     disabled={process}
                   />
-                  {error != null && (
-                    <Form.Control.Feedback type="invalid">
-                      {error}
-                    </Form.Control.Feedback>
-                  )}
-                </Form.Group>
+                  {error != null && <div>{error}</div>}
+                </div>
               );
             }
           })}
@@ -412,7 +399,7 @@ const OperationDialog = <
             onClick={close}
           />
           <LoadingButton
-            variant={props.themeColor}
+            color={props.themeColor}
             loading={process}
             disabled={!canProcess}
             onClick={() => {
diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx
index 6c0aaf67..6d70c64a 100644
--- a/FrontEnd/src/views/login/index.tsx
+++ b/FrontEnd/src/views/login/index.tsx
@@ -81,67 +81,59 @@ const LoginPage: React.FC = (_) => {
     <div className="login-container container-fluid mt-2">
       <h1 className="text-center">{t("welcome")}</h1>
       <div>
-          <Form.Label htmlFor="username">{t("user.username")}</Form.Label>
-          <Form.Control
-            id="username"
-            disabled={process}
-            onChange={(e) => {
-              setUsername(e.target.value);
-              setUsernameDirty(true);
-            }}
-            value={username}
-            isInvalid={usernameDirty && username === ""}
-          />
-          {usernameDirty && username === "" && (
-            <Form.Control.Feedback type="invalid">
-              {t("login.emptyUsername")}
-            </Form.Control.Feedback>
-          )}
-          <Form.Label htmlFor="password">{t("user.password")}</Form.Label>
-          <Form.Control
-            id="password"
-            type="password"
-            disabled={process}
-            onChange={(e) => {
-              setPassword(e.target.value);
-              setPasswordDirty(true);
-            }}
-            value={password}
-            onKeyDown={onEnterPressInPassword}
-            isInvalid={passwordDirty && password === ""}
-          />
-          {passwordDirty && password === "" && (
-            <Form.Control.Feedback type="invalid">
-              {t("login.emptyPassword")}
-            </Form.Control.Feedback>
-          )}
-        </Form.Group>
-        <Form.Group>
-          <Form.Check<"input">
-            id="remember-me"
-            type="checkbox"
-            checked={rememberMe}
-            onChange={(e) => {
-              setRememberMe(e.currentTarget.checked);
-            }}
-            label={t("user.rememberMe")}
-          />
-        </Form.Group>
-        {error ? <p className="text-danger">{t(error)}</p> : null}
-        <div className="text-end">
-          <LoadingButton
-            loading={process}
-            variant="primary"
-            onClick={(e) => {
-              submit();
-              e.preventDefault();
-            }}
-            disabled={username === "" || password === "" ? true : undefined}
-          >
-            {t("user.login")}
-          </LoadingButton>
-        </div>
-      </Form>
+        <label htmlFor="username">{t("user.username")}</label>
+        <input
+          id="username"
+          disabled={process}
+          onChange={(e) => {
+            setUsername(e.target.value);
+            setUsernameDirty(true);
+          }}
+          value={username}
+        />
+        {usernameDirty && username === "" && (
+          <div>{t("login.emptyUsername")}</div>
+        )}
+        <label htmlFor="password">{t("user.password")}</label>
+        <input
+          id="password"
+          type="password"
+          disabled={process}
+          onChange={(e) => {
+            setPassword(e.target.value);
+            setPasswordDirty(true);
+          }}
+          value={password}
+          onKeyDown={onEnterPressInPassword}
+        />
+        {passwordDirty && password === "" && (
+          <div>{t("login.emptyPassword")}</div>
+        )}
+      </div>
+      <div>
+        <input
+          id="remember-me"
+          type="checkbox"
+          checked={rememberMe}
+          onChange={(e) => {
+            setRememberMe(e.currentTarget.checked);
+          }}
+        />
+        <label>{t("user.rememberMe")}</label>
+      </div>
+      {error ? <p className="text-danger">{t(error)}</p> : null}
+      <div className="text-end">
+        <LoadingButton
+          loading={process}
+          onClick={(e) => {
+            submit();
+            e.preventDefault();
+          }}
+          disabled={username === "" || password === "" ? true : undefined}
+        >
+          {t("user.login")}
+        </LoadingButton>
+      </div>
     </div>
   );
 };
diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
index 6cb64dd3..9f0753b9 100644
--- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
@@ -1,15 +1,17 @@
+/* eslint-disable react/jsx-no-undef */
 import React from "react";
 import classnames from "classnames";
-import { Form, Spinner } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { Prompt } from "react-router";
 
 import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
+import TimelinePostBuilder from "@/services/TimelinePostBuilder";
+
 import FlatButton from "../common/button/FlatButton";
 import TabPages from "../common/TabPages";
-import TimelinePostBuilder from "@/services/TimelinePostBuilder";
 import ConfirmDialog from "../common/dailog/ConfirmDialog";
+import Spinner from "../common/Spinner";
 
 export interface MarkdownPostEditProps {
   timeline: string;
@@ -102,7 +104,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
         pageContainerClassName="py-2"
         actions={
           process ? (
-            <Spinner variant="primary" animation="border" size="sm" />
+            <Spinner />
           ) : (
             <>
               <FlatButton
diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx
index 830ecd45..c8f26da7 100644
--- a/FrontEnd/src/views/timeline-common/TimelineMember.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx
@@ -5,21 +5,20 @@ import { convertI18nText, I18nText } from "@/common";
 
 import { HttpUser } from "@/http/user";
 import { getHttpSearchClient } from "@/http/search";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
 
 import SearchInput from "../common/SearchInput";
 import UserAvatar from "../common/user/UserAvatar";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
 import Button from "../common/button/Button";
+import Dialog from "../common/dailog/Dialog";
 
 const TimelineMemberItem: React.FC<{
   user: HttpUser;
   add?: boolean;
   onAction?: (username: string) => void;
 }> = ({ user, add, onAction }) => {
-  const { t } = useTranslation();
-
   return (
-    <ListGroup.Item className="container">
+    <div className="container">
       <div className="row">
         <div className="col col-auto">
           <UserAvatar username={user.username} className="avatar small" />
@@ -42,7 +41,7 @@ const TimelineMemberItem: React.FC<{
           </div>
         ) : null}
       </div>
-    </ListGroup.Item>
+    </div>
   );
 };
 
@@ -109,7 +108,7 @@ const TimelineMemberUserSearch: React.FC<{
             return <div>{t("timeline.member.noUserAvailableToAdd")}</div>;
           } else {
             return (
-              <ListGroup className="mt-2">
+              <div className="mt-2">
                 {users.map((user) => (
                   <TimelineMemberItem
                     key={user.username}
@@ -126,7 +125,7 @@ const TimelineMemberUserSearch: React.FC<{
                     }}
                   />
                 ))}
-              </ListGroup>
+              </div>
             );
           }
         } else if (userSearchState.type === "error") {
@@ -152,7 +151,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
 
   return (
     <div className="container px-4 py-3">
-      <ListGroup>
+      <div>
         {members.map((member, index) => (
           <TimelineMemberItem
             key={member.username}
@@ -168,7 +167,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
             }
           />
         ))}
-      </ListGroup>
+      </div>
       {timeline.manageable ? (
         <TimelineMemberUserSearch timeline={timeline} onChange={onChange} />
       ) : null}
@@ -187,8 +186,8 @@ export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = (
   props
 ) => {
   return (
-    <Modal show centered onHide={props.onClose}>
+    <Dialog open onClose={props.onClose}>
       <TimelineMember {...props} />
-    </Modal>
+    </Dialog>
   );
 };
diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
index d05f18d4..ea6e8d40 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
@@ -1,5 +1,4 @@
 import React from "react";
-import { Container } from "react-bootstrap";
 import { HubConnectionState } from "@microsoft/signalr";
 
 import { HttpTimelineInfo } from "@/http/timeline";
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx
deleted file mode 100644
index e04bb7e1..00000000
--- a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import Button from "../common/button/Button";
-
-const TimelinePostDeleteConfirmDialog: React.FC<{
-  onClose: () => void;
-  onConfirm: () => void;
-}> = ({ onClose, onConfirm }) => {
-  const { t } = useTranslation();
-
-  return (
-    <Modal onHide={onClose} show centered>
-      <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
-          text="operationDialog.cancel"
-          color="secondary"
-          onClick={onClose}
-        />
-        <Button
-          variant="danger"
-          onClick={() => {
-            onConfirm();
-            onClose();
-          }}
-        >
-          {t("operationDialog.confirm")}
-        </Button>
-      </Modal.Footer>
-    </Modal>
-  );
-};
-
-export default TimelinePostDeleteConfirmDialog;
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
index 5e59bee4..13aacb54 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
@@ -265,7 +265,6 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
                 </PopupMenu>
               </div>
               <LoadingButton
-                variant="primary"
                 onClick={onSend}
                 disabled={!canSend}
                 loading={process}
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx
index 7b16e898..995c43df 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx
@@ -9,9 +9,9 @@ import { pushAlert } from "@/services/alert";
 import UserAvatar from "../common/user/UserAvatar";
 import Card from "../common/Card";
 import FlatButton from "../common/button/FlatButton";
+import ConfirmDialog from "../common/dailog/ConfirmDialog";
 import TimelineLine from "./TimelineLine";
 import TimelinePostContentView from "./TimelinePostContentView";
-import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog";
 import PostPropertyChangeDialog from "./PostPropertyChangeDialog";
 
 export interface TimelinePostViewProps {
@@ -116,7 +116,9 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => {
         ) : null}
       </Card>
       {dialog === "delete" ? (
-        <TimelinePostDeleteConfirmDialog
+        <ConfirmDialog
+          title="timeline.post.deleteDialog.title"
+          body="timeline.post.deleteDialog.prompt"
           onClose={() => {
             setDialog(null);
             setOperationMaskVisible(false);
-- 
cgit v1.2.3


From 1d320f3e363d46900a420650360b68ba9a4f3f2f Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Wed, 30 Jun 2021 23:02:41 +0800
Subject: ...

---
 FrontEnd/src/views/common/Spinner.tsx              | 12 ++-
 FrontEnd/src/views/common/button/Button.css        | 99 ++++++++++++----------
 FrontEnd/src/views/common/button/LoadingButton.tsx | 11 ++-
 FrontEnd/src/views/common/index.css                |  8 ++
 4 files changed, 80 insertions(+), 50 deletions(-)

(limited to 'FrontEnd/src/views/common/button/LoadingButton.tsx')

diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx
index b591d8ab..4c735fef 100644
--- a/FrontEnd/src/views/common/Spinner.tsx
+++ b/FrontEnd/src/views/common/Spinner.tsx
@@ -8,12 +8,14 @@ import "./Spinner.css";
 export interface SpinnerProps {
   size?: "sm" | "md" | "lg" | number | string;
   color?: PaletteColorType;
+  className?: string;
+  style?: React.CSSProperties;
 }
 
 export default function Spinner(
   props: SpinnerProps
 ): React.ReactElement | null {
-  const { size, color } = props;
+  const { size, color, className, style } = props;
   const calculatedSize =
     size === "sm"
       ? "18px"
@@ -30,8 +32,12 @@ export default function Spinner(
 
   return (
     <span
-      className={classnames("cru-spinner", `cru-color-${calculatedColor}`)}
-      style={{ width: calculatedSize, height: calculatedSize }}
+      className={classnames(
+        "cru-spinner",
+        `cru-color-${calculatedColor}`,
+        className
+      )}
+      style={{ width: calculatedSize, height: calculatedSize, ...style }}
     />
   );
 }
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css
index 8b5b74a7..3e408d8d 100644
--- a/FrontEnd/src/views/common/button/Button.css
+++ b/FrontEnd/src/views/common/button/Button.css
@@ -1,72 +1,81 @@
-.cru-button {
-  color: white;
-  cursor: pointer;
-  padding: 0.2em 0.5em;
-  border-radius: 0.2em;
-  border: none;
-  transition: all 0.6s;
-}
-
 .cru-button.primary {
-  background-color: var(--cru-primary-color);
-}
-
-.cru-button.primary:hover {
-  background-color: var(--cru-primary-f1-color);
+  --cru-button-color: var(--cru-primary-color);
+  --cru-button-f1-color: var(--cru-primary-f1-color);
+  --cru-button-f2-color: var(--cru-primary-f2-color);
+  --cru-button-f3-color: var(--cru-primary-f3-color);
 }
 
-.cru-button.primary:active {
-  background-color: var(--cru-primary-f2-color);
-}
-
-.cru-button.primary.disabled {
-  background-color: var(--cru-primary-f3-color);
+.cru-button.primary-enhance {
+  --cru-button-color: var(--cru-primary-enhance-color);
+  --cru-button-f1-color: var(--cru-primary-enhance-f1-color);
+  --cru-button-f2-color: var(--cru-primary-enhance-f2-color);
+  --cru-button-f3-color: var(--cru-primary-enhance-f3-color);
 }
 
 .cru-button.secondary {
-  background-color: var(--cru-secondary-color);
-}
-
-.cru-button.secondary:hover {
-  background-color: var(--cru-secondary-f1-color);
+  --cru-button-color: var(--cru-secondary-color);
+  --cru-button-f1-color: var(--cru-secondary-f1-color);
+  --cru-button-f2-color: var(--cru-secondary-f2-color);
+  --cru-button-f3-color: var(--cru-secondary-f3-color);
 }
 
-.cru-button.secondary:active {
-  background-color: var(--cru-secondary-f2-color);
+.cru-button.success {
+  --cru-button-color: var(--cru-success-color);
+  --cru-button-f1-color: var(--cru-success-f1-color);
+  --cru-button-f2-color: var(--cru-success-f2-color);
+  --cru-button-f3-color: var(--cru-success-f3-color);
 }
 
-.cru-button.secondary.disabled {
-  background-color: var(--cru-secondary-f3-color);
+.cru-button.danger {
+  --cru-button-color: var(--cru-danger-color);
+  --cru-button-f1-color: var(--cru-danger-f1-color);
+  --cru-button-f2-color: var(--cru-danger-f2-color);
+  --cru-button-f3-color: var(--cru-danger-f3-color);
 }
 
-.cru-button.success {
-  background-color: var(--cru-success-color);
+.cru-button:not(.outline) {
+  color: white;
+  cursor: pointer;
+  padding: 0.2em 0.5em;
+  border-radius: 0.2em;
+  border: none;
+  transition: all 0.5s;
+  background-color: var(--cru-button-color);
 }
 
-.cru-button.success:hover {
-  background-color: var(--cru-success-f1-color);
+.cru-button:not(.outline):hover {
+  background-color: var(--cru-button-f1-color);
 }
 
-.cru-button.success:active {
-  background-color: var(--cru-success-f2-color);
+.cru-button:not(.outline):active {
+  background-color: var(--cru-button-f2-color);
 }
 
-.cru-button.success.disabled {
-  background-color: var(--cru-success-f3-color);
+.cru-button:not(.outline):disabled {
+  background-color: var(--cru-button-f3-color);
 }
 
-.cru-button.danger {
-  background-color: var(--cru-danger-color);
+.cru-button.outline {
+  color: var(--cru-button-color);
+  border: var(--cru-button-color) 1px solid;
+  cursor: pointer;
+  padding: 0.2em 0.5em;
+  border-radius: 0.2em;
+  transition: all 0.6s;
+  background-color: white;
 }
 
-.cru-button.danger:hover {
-  background-color: var(--cru-danger-f1-color);
+.cru-button.outline:hover {
+  color: var(--cru-button-f1-color);
+  border-color: var(--cru-button-f1-color);
 }
 
-.cru-button.danger:active {
-  background-color: var(--cru-danger-f2-color);
+.cru-button.outline:active {
+  color: var(--cru-button-f2-color);
+  border-color: var(--cru-button-f2-color);
 }
 
-.cru-button.danger.disabled {
-  background-color: var(--cru-danger-f3-color);
+.cru-button.outline:disabled {
+  color: var(--cru-button-f3-color);
+  border-color: var(--cru-button-f3-color);
 }
diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
index aee83aa2..a7e34f91 100644
--- a/FrontEnd/src/views/common/button/LoadingButton.tsx
+++ b/FrontEnd/src/views/common/button/LoadingButton.tsx
@@ -11,9 +11,16 @@ const LoadingButton: React.FC<{ loading?: boolean } & CommonButtonProps> = ({
   ...otherProps
 }) => {
   return (
-    <Button color={color} disabled={disabled || loading} {...otherProps}>
+    <Button
+      color={color}
+      outline
+      disabled={disabled || loading}
+      {...otherProps}
+    >
       {otherProps.children}
-      {loading ? <Spinner color={color} /> : null}
+      {loading ? (
+        <Spinner className="cru-align-text-bottom ms-1" color={color} />
+      ) : null}
     </Button>
   );
 };
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
index 4a860e95..8eb24e1d 100644
--- a/FrontEnd/src/views/common/index.css
+++ b/FrontEnd/src/views/common/index.css
@@ -144,6 +144,14 @@
   float: right;
 }
 
+.cru-align-text-bottom {
+  vertical-align: text-bottom;
+}
+
+.cru-align-middle {
+  vertical-align: middle;
+}
+
 .cru-clearfix::after {
   clear: both;
 }
-- 
cgit v1.2.3

