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/common/Spinner.tsx | 13 +++++++++++++
 1 file changed, 13 insertions(+)
 create mode 100644 FrontEnd/src/views/common/Spinner.tsx

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

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 />;
+}
-- 
cgit v1.2.3


From fdc2d4a971d608bb230cd8aa1e602197c7775231 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Wed, 30 Jun 2021 22:46:42 +0800
Subject: ...

---
 FrontEnd/src/views/center/TimelineBoard.tsx | 26 ++++++++++++++-----------
 FrontEnd/src/views/common/Spinner.css       | 13 +++++++++++++
 FrontEnd/src/views/common/Spinner.tsx       | 30 ++++++++++++++++++++++++++---
 FrontEnd/vite.config.js                     |  2 +-
 4 files changed, 56 insertions(+), 15 deletions(-)
 create mode 100644 FrontEnd/src/views/common/Spinner.css

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

diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx
index d7aa39ab..3961a7bc 100644
--- a/FrontEnd/src/views/center/TimelineBoard.tsx
+++ b/FrontEnd/src/views/center/TimelineBoard.tsx
@@ -208,7 +208,8 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({
 
 interface TimelineBoardUIProps {
   title?: string;
-  timelines: HttpTimelineInfo[] | "offline" | "loading";
+  state: "offline" | "loading" | "loaded";
+  timelines: HttpTimelineInfo[];
   onReload: () => void;
   className?: string;
   editHandler?: {
@@ -218,7 +219,7 @@ interface TimelineBoardUIProps {
 }
 
 const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
-  const { title, timelines, className, editHandler } = props;
+  const { title, state, timelines, className, editHandler } = props;
 
   const editable = editHandler != null;
 
@@ -246,13 +247,13 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
           ))}
       </div>
       {(() => {
-        if (timelines === "loading") {
+        if (state === "loading") {
           return (
             <div className="d-flex flex-grow-1 justify-content-center align-items-center">
               <Spinner />
             </div>
           );
-        } else if (timelines === "offline") {
+        } else if (state === "offline") {
           return (
             <div className="d-flex flex-grow-1 justify-content-center align-items-center">
               <LoadFailReload onReload={props.onReload} />
@@ -301,36 +302,39 @@ const TimelineBoard: React.FC<TimelineBoardProps> = ({
   load,
   editHandler,
 }) => {
-  const [timelines, setTimelines] = React.useState<
-    HttpTimelineInfo[] | "offline" | "loading"
-  >("loading");
+  const [state, setState] = React.useState<"offline" | "loading" | "loaded">(
+    "loading"
+  );
+  const [timelines, setTimelines] = React.useState<HttpTimelineInfo[]>([]);
 
   React.useEffect(() => {
     let subscribe = true;
-    if (timelines === "loading") {
+    if (state === "loading") {
       void load().then(
         (timelines) => {
           if (subscribe) {
+            setState("loaded");
             setTimelines(timelines);
           }
         },
         () => {
-          setTimelines("offline");
+          setState("offline");
         }
       );
     }
     return () => {
       subscribe = false;
     };
-  }, [load, timelines]);
+  }, [load, state]);
 
   return (
     <TimelineBoardUI
       title={title}
       className={className}
+      state={state}
       timelines={timelines}
       onReload={() => {
-        setTimelines("loading");
+        setState("loaded");
       }}
       editHandler={
         typeof timelines === "object" && editHandler != null
diff --git a/FrontEnd/src/views/common/Spinner.css b/FrontEnd/src/views/common/Spinner.css
new file mode 100644
index 00000000..a1de68d2
--- /dev/null
+++ b/FrontEnd/src/views/common/Spinner.css
@@ -0,0 +1,13 @@
+@keyframes cru-spinner-animation {
+  from {
+    transform: scale(0,0);
+  }
+}
+
+.cru-spinner {
+  display: inline-block;
+  animation: cru-spinner-animation 0.5s infinite alternate;
+  background-color: currentColor;
+  border-radius: 50%;
+  transform-origin: center;
+}
diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx
index 783c9be2..b591d8ab 100644
--- a/FrontEnd/src/views/common/Spinner.tsx
+++ b/FrontEnd/src/views/common/Spinner.tsx
@@ -1,13 +1,37 @@
-import { PaletteColorType } from "@/palette";
 import React from "react";
+import classnames from "classnames";
+
+import { PaletteColorType } from "@/palette";
+
+import "./Spinner.css";
 
 export interface SpinnerProps {
-  size?: "sm" | "md" | "lg" | number;
+  size?: "sm" | "md" | "lg" | number | string;
   color?: PaletteColorType;
 }
 
 export default function Spinner(
   props: SpinnerProps
 ): React.ReactElement | null {
-  return <span />;
+  const { size, color } = props;
+  const calculatedSize =
+    size === "sm"
+      ? "18px"
+      : size === "md"
+      ? "30px"
+      : size === "lg"
+      ? "42px"
+      : typeof size === "number"
+      ? size
+      : size == null
+      ? "20px"
+      : size;
+  const calculatedColor = color ?? "primary";
+
+  return (
+    <span
+      className={classnames("cru-spinner", `cru-color-${calculatedColor}`)}
+      style={{ width: calculatedSize, height: calculatedSize }}
+    />
+  );
 }
diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js
index 88529a33..64e0457f 100644
--- a/FrontEnd/vite.config.js
+++ b/FrontEnd/vite.config.js
@@ -22,7 +22,7 @@ export default defineConfig({
     alias: [{ find: "@", replacement: "/src" }],
   },
   server: {
-    port: 13000,
+    port: 10030,
     proxy: {
       "/api": {
         target: "http://localhost:5000",
-- 
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/Spinner.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

