From e6ccc0174a86a0ade240e6551228598cd81f984b Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Tue, 1 Aug 2023 00:29:35 +0800
Subject: ...

---
 FrontEnd/src/pages/timeline/TimelinePostList.tsx | 76 ++++++++++++++++++++++++
 1 file changed, 76 insertions(+)
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostList.tsx

(limited to 'FrontEnd/src/pages/timeline/TimelinePostList.tsx')

diff --git a/FrontEnd/src/pages/timeline/TimelinePostList.tsx b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
new file mode 100644
index 00000000..a3501b33
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
@@ -0,0 +1,76 @@
+import { useMemo, Fragment } from "react";
+
+import { HttpTimelinePostInfo } from "@/http/timeline";
+
+import TimelinePostView from "./TimelinePostView";
+import TimelineDateLabel from "./TimelineDateLabel";
+
+import "./TimelinePostList.css";
+
+function dateEqual(left: Date, right: Date): boolean {
+  return (
+    left.getDate() == right.getDate() &&
+    left.getMonth() == right.getMonth() &&
+    left.getFullYear() == right.getFullYear()
+  );
+}
+
+interface TimelinePostListViewProps {
+  posts: HttpTimelinePostInfo[];
+  onReload: () => void;
+}
+
+export default function TimelinePostList(props: TimelinePostListViewProps) {
+  const { posts, onReload } = props;
+
+  const groupedPosts = useMemo<
+    {
+      date: Date;
+      posts: (HttpTimelinePostInfo & { index: number })[];
+    }[]
+  >(() => {
+    const result: {
+      date: Date;
+      posts: (HttpTimelinePostInfo & { index: number })[];
+    }[] = [];
+    let index = 0;
+    for (const post of posts) {
+      const time = new Date(post.time);
+      if (result.length === 0) {
+        result.push({ date: time, posts: [{ ...post, index }] });
+      } else {
+        const lastGroup = result[result.length - 1];
+        if (dateEqual(lastGroup.date, time)) {
+          lastGroup.posts.push({ ...post, index });
+        } else {
+          result.push({ date: time, posts: [{ ...post, index }] });
+        }
+      }
+      index++;
+    }
+    return result;
+  }, [posts]);
+
+  return (
+    <div>
+      <div className="timeline-post-timeline" />
+      {groupedPosts.map((group) => {
+        return (
+          <Fragment key={group.date.toDateString()}>
+            <TimelineDateLabel date={group.date} />
+            {group.posts.map((post) => {
+              return (
+                <TimelinePostView
+                  key={post.id}
+                  post={post}
+                  onChanged={onReload}
+                  onDeleted={onReload}
+                />
+              );
+            })}
+          </Fragment>
+        );
+      })}
+    </div>
+  );
+}
-- 
cgit v1.2.3


From f5dfd52f6efece2f4cad227044ecf4dd66301bbc Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Aug 2023 21:36:58 +0800
Subject: ...

---
 FrontEnd/src/App.tsx                               |   4 +-
 FrontEnd/src/components/AppBar.css                 |  87 ++++
 FrontEnd/src/components/AppBar.tsx                 |  98 +++++
 FrontEnd/src/components/BlobImage.tsx              |  26 ++
 FrontEnd/src/components/Card.css                   |  20 +
 FrontEnd/src/components/Card.tsx                   |  38 ++
 FrontEnd/src/components/Icon.css                   |   3 +
 FrontEnd/src/components/Icon.tsx                   |  30 ++
 FrontEnd/src/components/ImageCropper.css           |  38 ++
 FrontEnd/src/components/ImageCropper.tsx           | 312 ++++++++++++++
 FrontEnd/src/components/LoadFailReload.tsx         |  37 ++
 FrontEnd/src/components/LoadingPage.tsx            |  13 +
 FrontEnd/src/components/Page.tsx                   |  15 +
 FrontEnd/src/components/SearchInput.css            |   8 +
 FrontEnd/src/components/SearchInput.tsx            |  50 +++
 FrontEnd/src/components/Skeleton.css               |  14 +
 FrontEnd/src/components/Skeleton.tsx               |  32 ++
 FrontEnd/src/components/Spinner.css                |  13 +
 FrontEnd/src/components/Spinner.tsx                |  36 ++
 FrontEnd/src/components/TimelineLogo.tsx           |  27 ++
 FrontEnd/src/components/alert/AlertHost.tsx        | 113 +++++
 FrontEnd/src/components/alert/alert.css            |  33 ++
 FrontEnd/src/components/breakpoints.ts             |   3 +
 FrontEnd/src/components/button/Button.css          |  64 +++
 FrontEnd/src/components/button/Button.tsx          |  46 ++
 FrontEnd/src/components/button/ButtonRow.css       |   0
 FrontEnd/src/components/button/ButtonRow.tsx       |  62 +++
 FrontEnd/src/components/button/ButtonRowV2.tsx     | 143 +++++++
 FrontEnd/src/components/button/FlatButton.css      |  27 ++
 FrontEnd/src/components/button/FlatButton.tsx      |  36 ++
 FrontEnd/src/components/button/IconButton.css      |  30 ++
 FrontEnd/src/components/button/IconButton.tsx      |  30 ++
 FrontEnd/src/components/button/LoadingButton.css   |  13 +
 FrontEnd/src/components/button/LoadingButton.tsx   |  40 ++
 FrontEnd/src/components/button/index.tsx           |  15 +
 FrontEnd/src/components/common.ts                  |  14 +
 FrontEnd/src/components/dialog/ConfirmDialog.css   |   0
 FrontEnd/src/components/dialog/ConfirmDialog.tsx   |  59 +++
 FrontEnd/src/components/dialog/Dialog.css          |  60 +++
 FrontEnd/src/components/dialog/Dialog.tsx          |  63 +++
 FrontEnd/src/components/dialog/DialogContainer.css |  20 +
 FrontEnd/src/components/dialog/DialogContainer.tsx |  95 +++++
 FrontEnd/src/components/dialog/FullPageDialog.css  |  44 ++
 FrontEnd/src/components/dialog/FullPageDialog.tsx  |  53 +++
 FrontEnd/src/components/dialog/OperationDialog.css |   8 +
 FrontEnd/src/components/dialog/OperationDialog.tsx | 230 ++++++++++
 FrontEnd/src/components/dialog/index.ts            |  64 +++
 FrontEnd/src/components/hooks.ts                   |  14 +
 FrontEnd/src/components/index.css                  | 100 +++++
 FrontEnd/src/components/input/InputGroup.css       |  54 +++
 FrontEnd/src/components/input/InputGroup.tsx       | 463 +++++++++++++++++++++
 FrontEnd/src/components/input/index.ts             |  11 +
 FrontEnd/src/components/list/ListContainer.css     |   4 +
 FrontEnd/src/components/list/ListContainer.tsx     |  23 +
 FrontEnd/src/components/list/ListItemContainer.css |   3 +
 FrontEnd/src/components/list/ListItemContainer.tsx |  23 +
 FrontEnd/src/components/list/index.ts              |   4 +
 FrontEnd/src/components/menu/Menu.css              |  36 ++
 FrontEnd/src/components/menu/Menu.tsx              |  62 +++
 FrontEnd/src/components/menu/PopupMenu.css         |   7 +
 FrontEnd/src/components/menu/PopupMenu.tsx         |  73 ++++
 FrontEnd/src/components/tab/TabPages.tsx           |  71 ++++
 FrontEnd/src/components/tab/Tabs.css               |  33 ++
 FrontEnd/src/components/tab/Tabs.tsx               |  62 +++
 FrontEnd/src/components/theme-color.css            | 173 ++++++++
 FrontEnd/src/components/theme.css                  | 146 +++++++
 FrontEnd/src/components/user/UserAvatar.tsx        |  22 +
 FrontEnd/src/http/bookmark.ts                      |   2 +-
 FrontEnd/src/http/timeline.ts                      |   2 +-
 FrontEnd/src/migrating/center/CenterBoards.tsx     |  10 +-
 FrontEnd/src/migrating/center/TimelineBoard.tsx    |   2 +-
 .../src/migrating/center/TimelineCreateDialog.tsx  |   6 +-
 FrontEnd/src/migrating/center/index.tsx            |   2 +-
 FrontEnd/src/pages/about/index.tsx                 |   4 +-
 FrontEnd/src/pages/loading/index.tsx               |   2 +-
 FrontEnd/src/pages/login/index.tsx                 |  10 +-
 FrontEnd/src/pages/register/index.tsx              |  10 +-
 FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx  |  16 +-
 .../src/pages/setting/ChangeNicknameDialog.tsx     |   8 +-
 .../src/pages/setting/ChangePasswordDialog.tsx     |   9 +-
 FrontEnd/src/pages/setting/index.tsx               |  18 +-
 FrontEnd/src/pages/timeline/CollapseButton.tsx     |   2 +-
 FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx   |  14 +-
 FrontEnd/src/pages/timeline/Timeline.tsx           |   8 +-
 FrontEnd/src/pages/timeline/TimelineCard.tsx       |  22 +-
 .../src/pages/timeline/TimelineDeleteDialog.tsx    |   4 +-
 FrontEnd/src/pages/timeline/TimelineMember.tsx     |  16 +-
 FrontEnd/src/pages/timeline/TimelinePostCard.tsx   |   2 +-
 .../src/pages/timeline/TimelinePostContentView.tsx |  12 +-
 .../src/pages/timeline/TimelinePostCreateView.tsx  |  16 +-
 FrontEnd/src/pages/timeline/TimelinePostList.tsx   |   2 +-
 FrontEnd/src/pages/timeline/TimelinePostView.tsx   |  16 +-
 .../timeline/TimelinePropertyChangeDialog.tsx      |  10 +-
 FrontEnd/src/pages/timeline/index.tsx              |   2 +-
 FrontEnd/src/services/TimelinePostBuilder.ts       |   6 +-
 FrontEnd/src/services/alert.ts                     |   4 +-
 FrontEnd/src/services/timeline.ts                  |   4 +-
 FrontEnd/src/services/user.ts                      |   8 +-
 FrontEnd/src/views/common/AppBar.css               |  87 ----
 FrontEnd/src/views/common/AppBar.tsx               |  98 -----
 FrontEnd/src/views/common/BlobImage.tsx            |  26 --
 FrontEnd/src/views/common/Card.css                 |  20 -
 FrontEnd/src/views/common/Card.tsx                 |  38 --
 FrontEnd/src/views/common/Icon.css                 |   3 -
 FrontEnd/src/views/common/Icon.tsx                 |  30 --
 FrontEnd/src/views/common/ImageCropper.css         |  38 --
 FrontEnd/src/views/common/ImageCropper.tsx         | 312 --------------
 FrontEnd/src/views/common/LoadFailReload.tsx       |  37 --
 FrontEnd/src/views/common/LoadingPage.tsx          |  13 -
 FrontEnd/src/views/common/Page.tsx                 |  15 -
 FrontEnd/src/views/common/SearchInput.css          |   8 -
 FrontEnd/src/views/common/SearchInput.tsx          |  50 ---
 FrontEnd/src/views/common/Skeleton.css             |  14 -
 FrontEnd/src/views/common/Skeleton.tsx             |  32 --
 FrontEnd/src/views/common/Spinner.css              |  13 -
 FrontEnd/src/views/common/Spinner.tsx              |  36 --
 FrontEnd/src/views/common/TimelineLogo.tsx         |  27 --
 FrontEnd/src/views/common/alert/AlertHost.tsx      | 113 -----
 FrontEnd/src/views/common/alert/alert.css          |  33 --
 FrontEnd/src/views/common/breakpoints.ts           |   3 -
 FrontEnd/src/views/common/button/Button.css        |  64 ---
 FrontEnd/src/views/common/button/Button.tsx        |  46 --
 FrontEnd/src/views/common/button/ButtonRow.css     |   0
 FrontEnd/src/views/common/button/ButtonRow.tsx     |  62 ---
 FrontEnd/src/views/common/button/ButtonRowV2.tsx   | 143 -------
 FrontEnd/src/views/common/button/FlatButton.css    |  27 --
 FrontEnd/src/views/common/button/FlatButton.tsx    |  36 --
 FrontEnd/src/views/common/button/IconButton.css    |  30 --
 FrontEnd/src/views/common/button/IconButton.tsx    |  30 --
 FrontEnd/src/views/common/button/LoadingButton.css |  13 -
 FrontEnd/src/views/common/button/LoadingButton.tsx |  40 --
 FrontEnd/src/views/common/button/index.tsx         |  15 -
 FrontEnd/src/views/common/common.ts                |  14 -
 FrontEnd/src/views/common/dialog/ConfirmDialog.css |   0
 FrontEnd/src/views/common/dialog/ConfirmDialog.tsx |  59 ---
 FrontEnd/src/views/common/dialog/Dialog.css        |  60 ---
 FrontEnd/src/views/common/dialog/Dialog.tsx        |  63 ---
 .../src/views/common/dialog/DialogContainer.css    |  20 -
 .../src/views/common/dialog/DialogContainer.tsx    |  95 -----
 .../src/views/common/dialog/FullPageDialog.css     |  44 --
 .../src/views/common/dialog/FullPageDialog.tsx     |  53 ---
 .../src/views/common/dialog/OperationDialog.css    |   8 -
 .../src/views/common/dialog/OperationDialog.tsx    | 228 ----------
 FrontEnd/src/views/common/dialog/index.ts          |  64 ---
 FrontEnd/src/views/common/hooks.ts                 |  14 -
 FrontEnd/src/views/common/index.css                | 100 -----
 FrontEnd/src/views/common/input/InputGroup.css     |  54 ---
 FrontEnd/src/views/common/input/InputGroup.tsx     | 461 --------------------
 FrontEnd/src/views/common/list/ListContainer.css   |   4 -
 FrontEnd/src/views/common/list/ListContainer.tsx   |  23 -
 .../src/views/common/list/ListItemContainer.css    |   3 -
 .../src/views/common/list/ListItemContainer.tsx    |  23 -
 FrontEnd/src/views/common/list/index.ts            |   4 -
 FrontEnd/src/views/common/menu/Menu.css            |  36 --
 FrontEnd/src/views/common/menu/Menu.tsx            |  62 ---
 FrontEnd/src/views/common/menu/PopupMenu.css       |   7 -
 FrontEnd/src/views/common/menu/PopupMenu.tsx       |  73 ----
 FrontEnd/src/views/common/tab/TabPages.tsx         |  71 ----
 FrontEnd/src/views/common/tab/Tabs.css             |  33 --
 FrontEnd/src/views/common/tab/Tabs.tsx             |  62 ---
 FrontEnd/src/views/common/theme-color.css          | 173 --------
 FrontEnd/src/views/common/theme.css                | 146 -------
 FrontEnd/src/views/common/user/UserAvatar.tsx      |  22 -
 FrontEnd/tsconfig.json                             |   3 -
 164 files changed, 3771 insertions(+), 3762 deletions(-)
 create mode 100644 FrontEnd/src/components/AppBar.css
 create mode 100644 FrontEnd/src/components/AppBar.tsx
 create mode 100644 FrontEnd/src/components/BlobImage.tsx
 create mode 100644 FrontEnd/src/components/Card.css
 create mode 100644 FrontEnd/src/components/Card.tsx
 create mode 100644 FrontEnd/src/components/Icon.css
 create mode 100644 FrontEnd/src/components/Icon.tsx
 create mode 100644 FrontEnd/src/components/ImageCropper.css
 create mode 100644 FrontEnd/src/components/ImageCropper.tsx
 create mode 100644 FrontEnd/src/components/LoadFailReload.tsx
 create mode 100644 FrontEnd/src/components/LoadingPage.tsx
 create mode 100644 FrontEnd/src/components/Page.tsx
 create mode 100644 FrontEnd/src/components/SearchInput.css
 create mode 100644 FrontEnd/src/components/SearchInput.tsx
 create mode 100644 FrontEnd/src/components/Skeleton.css
 create mode 100644 FrontEnd/src/components/Skeleton.tsx
 create mode 100644 FrontEnd/src/components/Spinner.css
 create mode 100644 FrontEnd/src/components/Spinner.tsx
 create mode 100644 FrontEnd/src/components/TimelineLogo.tsx
 create mode 100644 FrontEnd/src/components/alert/AlertHost.tsx
 create mode 100644 FrontEnd/src/components/alert/alert.css
 create mode 100644 FrontEnd/src/components/breakpoints.ts
 create mode 100644 FrontEnd/src/components/button/Button.css
 create mode 100644 FrontEnd/src/components/button/Button.tsx
 create mode 100644 FrontEnd/src/components/button/ButtonRow.css
 create mode 100644 FrontEnd/src/components/button/ButtonRow.tsx
 create mode 100644 FrontEnd/src/components/button/ButtonRowV2.tsx
 create mode 100644 FrontEnd/src/components/button/FlatButton.css
 create mode 100644 FrontEnd/src/components/button/FlatButton.tsx
 create mode 100644 FrontEnd/src/components/button/IconButton.css
 create mode 100644 FrontEnd/src/components/button/IconButton.tsx
 create mode 100644 FrontEnd/src/components/button/LoadingButton.css
 create mode 100644 FrontEnd/src/components/button/LoadingButton.tsx
 create mode 100644 FrontEnd/src/components/button/index.tsx
 create mode 100644 FrontEnd/src/components/common.ts
 create mode 100644 FrontEnd/src/components/dialog/ConfirmDialog.css
 create mode 100644 FrontEnd/src/components/dialog/ConfirmDialog.tsx
 create mode 100644 FrontEnd/src/components/dialog/Dialog.css
 create mode 100644 FrontEnd/src/components/dialog/Dialog.tsx
 create mode 100644 FrontEnd/src/components/dialog/DialogContainer.css
 create mode 100644 FrontEnd/src/components/dialog/DialogContainer.tsx
 create mode 100644 FrontEnd/src/components/dialog/FullPageDialog.css
 create mode 100644 FrontEnd/src/components/dialog/FullPageDialog.tsx
 create mode 100644 FrontEnd/src/components/dialog/OperationDialog.css
 create mode 100644 FrontEnd/src/components/dialog/OperationDialog.tsx
 create mode 100644 FrontEnd/src/components/dialog/index.ts
 create mode 100644 FrontEnd/src/components/hooks.ts
 create mode 100644 FrontEnd/src/components/index.css
 create mode 100644 FrontEnd/src/components/input/InputGroup.css
 create mode 100644 FrontEnd/src/components/input/InputGroup.tsx
 create mode 100644 FrontEnd/src/components/input/index.ts
 create mode 100644 FrontEnd/src/components/list/ListContainer.css
 create mode 100644 FrontEnd/src/components/list/ListContainer.tsx
 create mode 100644 FrontEnd/src/components/list/ListItemContainer.css
 create mode 100644 FrontEnd/src/components/list/ListItemContainer.tsx
 create mode 100644 FrontEnd/src/components/list/index.ts
 create mode 100644 FrontEnd/src/components/menu/Menu.css
 create mode 100644 FrontEnd/src/components/menu/Menu.tsx
 create mode 100644 FrontEnd/src/components/menu/PopupMenu.css
 create mode 100644 FrontEnd/src/components/menu/PopupMenu.tsx
 create mode 100644 FrontEnd/src/components/tab/TabPages.tsx
 create mode 100644 FrontEnd/src/components/tab/Tabs.css
 create mode 100644 FrontEnd/src/components/tab/Tabs.tsx
 create mode 100644 FrontEnd/src/components/theme-color.css
 create mode 100644 FrontEnd/src/components/theme.css
 create mode 100644 FrontEnd/src/components/user/UserAvatar.tsx
 delete mode 100644 FrontEnd/src/views/common/AppBar.css
 delete mode 100644 FrontEnd/src/views/common/AppBar.tsx
 delete mode 100644 FrontEnd/src/views/common/BlobImage.tsx
 delete mode 100644 FrontEnd/src/views/common/Card.css
 delete mode 100644 FrontEnd/src/views/common/Card.tsx
 delete mode 100644 FrontEnd/src/views/common/Icon.css
 delete mode 100644 FrontEnd/src/views/common/Icon.tsx
 delete mode 100644 FrontEnd/src/views/common/ImageCropper.css
 delete mode 100644 FrontEnd/src/views/common/ImageCropper.tsx
 delete mode 100644 FrontEnd/src/views/common/LoadFailReload.tsx
 delete mode 100644 FrontEnd/src/views/common/LoadingPage.tsx
 delete mode 100644 FrontEnd/src/views/common/Page.tsx
 delete mode 100644 FrontEnd/src/views/common/SearchInput.css
 delete mode 100644 FrontEnd/src/views/common/SearchInput.tsx
 delete mode 100644 FrontEnd/src/views/common/Skeleton.css
 delete mode 100644 FrontEnd/src/views/common/Skeleton.tsx
 delete mode 100644 FrontEnd/src/views/common/Spinner.css
 delete mode 100644 FrontEnd/src/views/common/Spinner.tsx
 delete mode 100644 FrontEnd/src/views/common/TimelineLogo.tsx
 delete mode 100644 FrontEnd/src/views/common/alert/AlertHost.tsx
 delete mode 100644 FrontEnd/src/views/common/alert/alert.css
 delete mode 100644 FrontEnd/src/views/common/breakpoints.ts
 delete mode 100644 FrontEnd/src/views/common/button/Button.css
 delete mode 100644 FrontEnd/src/views/common/button/Button.tsx
 delete mode 100644 FrontEnd/src/views/common/button/ButtonRow.css
 delete mode 100644 FrontEnd/src/views/common/button/ButtonRow.tsx
 delete mode 100644 FrontEnd/src/views/common/button/ButtonRowV2.tsx
 delete mode 100644 FrontEnd/src/views/common/button/FlatButton.css
 delete mode 100644 FrontEnd/src/views/common/button/FlatButton.tsx
 delete mode 100644 FrontEnd/src/views/common/button/IconButton.css
 delete mode 100644 FrontEnd/src/views/common/button/IconButton.tsx
 delete mode 100644 FrontEnd/src/views/common/button/LoadingButton.css
 delete mode 100644 FrontEnd/src/views/common/button/LoadingButton.tsx
 delete mode 100644 FrontEnd/src/views/common/button/index.tsx
 delete mode 100644 FrontEnd/src/views/common/common.ts
 delete mode 100644 FrontEnd/src/views/common/dialog/ConfirmDialog.css
 delete mode 100644 FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
 delete mode 100644 FrontEnd/src/views/common/dialog/Dialog.css
 delete mode 100644 FrontEnd/src/views/common/dialog/Dialog.tsx
 delete mode 100644 FrontEnd/src/views/common/dialog/DialogContainer.css
 delete mode 100644 FrontEnd/src/views/common/dialog/DialogContainer.tsx
 delete mode 100644 FrontEnd/src/views/common/dialog/FullPageDialog.css
 delete mode 100644 FrontEnd/src/views/common/dialog/FullPageDialog.tsx
 delete mode 100644 FrontEnd/src/views/common/dialog/OperationDialog.css
 delete mode 100644 FrontEnd/src/views/common/dialog/OperationDialog.tsx
 delete mode 100644 FrontEnd/src/views/common/dialog/index.ts
 delete mode 100644 FrontEnd/src/views/common/hooks.ts
 delete mode 100644 FrontEnd/src/views/common/index.css
 delete mode 100644 FrontEnd/src/views/common/input/InputGroup.css
 delete mode 100644 FrontEnd/src/views/common/input/InputGroup.tsx
 delete mode 100644 FrontEnd/src/views/common/list/ListContainer.css
 delete mode 100644 FrontEnd/src/views/common/list/ListContainer.tsx
 delete mode 100644 FrontEnd/src/views/common/list/ListItemContainer.css
 delete mode 100644 FrontEnd/src/views/common/list/ListItemContainer.tsx
 delete mode 100644 FrontEnd/src/views/common/list/index.ts
 delete mode 100644 FrontEnd/src/views/common/menu/Menu.css
 delete mode 100644 FrontEnd/src/views/common/menu/Menu.tsx
 delete mode 100644 FrontEnd/src/views/common/menu/PopupMenu.css
 delete mode 100644 FrontEnd/src/views/common/menu/PopupMenu.tsx
 delete mode 100644 FrontEnd/src/views/common/tab/TabPages.tsx
 delete mode 100644 FrontEnd/src/views/common/tab/Tabs.css
 delete mode 100644 FrontEnd/src/views/common/tab/Tabs.tsx
 delete mode 100644 FrontEnd/src/views/common/theme-color.css
 delete mode 100644 FrontEnd/src/views/common/theme.css
 delete mode 100644 FrontEnd/src/views/common/user/UserAvatar.tsx

(limited to 'FrontEnd/src/pages/timeline/TimelinePostList.tsx')

diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx
index 6029daac..b5b076ec 100644
--- a/FrontEnd/src/App.tsx
+++ b/FrontEnd/src/App.tsx
@@ -1,7 +1,7 @@
 import { Suspense } from "react";
 import { BrowserRouter, Route, Routes } from "react-router-dom";
 
-import AppBar from "./views/common/AppBar";
+import AppBar from "./components/AppBar";
 import NotFoundPage from "./pages/404";
 import HomePage from "./pages/home";
 import AboutPage from "./pages/about";
@@ -10,7 +10,7 @@ import LoginPage from "./pages/login";
 import RegisterPage from "./pages/register";
 import TimelinePage from "./pages/timeline";
 import LoadingPage from "./pages/loading";
-import AlertHost from "./views/common/alert/AlertHost";
+import AlertHost from "./components/alert/AlertHost";
 
 export default function App() {
   return (
diff --git a/FrontEnd/src/components/AppBar.css b/FrontEnd/src/components/AppBar.css
new file mode 100644
index 00000000..a0d975b5
--- /dev/null
+++ b/FrontEnd/src/components/AppBar.css
@@ -0,0 +1,87 @@
+.app-bar {
+  height: 56px;
+  position: fixed;
+  z-index: 1030;
+  top: 0;
+  left: 0;
+  right: 0;
+  background-color: var(--cru-primary-color);
+}
+
+.app-bar {
+  display: flex;
+}
+
+.app-bar .app-bar-brand {
+  display: flex;
+  align-items: center;
+}
+
+.app-bar .app-bar-brand-icon {
+  height: 2em;
+}
+
+.app-bar .app-bar-user-area {
+  display: flex;
+  margin-left: auto;
+}
+
+.app-bar a {
+  background-color: var(--cru-primary-color);
+  color: var(--cru-push-button-text-color);
+  text-decoration: none;
+  display: flex;
+  align-items: center;
+  padding: 0 1em;
+  transition: all 0.5s;
+}
+
+.app-bar a:hover {
+  background-color: var(--cru-clickable-primary-hover-color);
+}
+
+.app-bar a:focus {
+  background-color: var(--cru-clickable-primary-focus-color);
+}
+
+.app-bar a:active {
+  background-color: var(--cru-clickable-primary-active-color);
+}
+
+/* the current page */
+.app-bar a.active {
+  background-color: var(--cru-clickable-primary-focus-color);
+}
+
+.app-bar .app-bar-avatar img {
+  width: 45px;
+  height: 45px;
+  background-color: white;
+  border-radius: 50%;
+}
+
+.app-bar.desktop .app-bar-link-area {
+  display: flex;
+}
+
+.app-bar.mobile .app-bar-link-area {
+  position: absolute;
+  z-index: -1;
+  top: 56px;
+  left: 0;
+  right: 0;
+  transition: transform 0.5s;
+}
+
+.app-bar.mobile a {
+  height: 56px;
+}
+
+.app-bar.mobile.collapse .app-bar-link-area {
+  transform: translateY(-100%);
+}
+
+.app-bar .toggler {
+  font-size: 2em;
+  margin-right: 0.5em;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/AppBar.tsx b/FrontEnd/src/components/AppBar.tsx
new file mode 100644
index 00000000..da3a946f
--- /dev/null
+++ b/FrontEnd/src/components/AppBar.tsx
@@ -0,0 +1,98 @@
+import { useState } from "react";
+import classnames from "classnames";
+import { Link, NavLink } from "react-router-dom";
+
+import { I18nText, useC, useMobile } from "./common";
+import { useUser } from "~src/services/user";
+
+import TimelineLogo from "./TimelineLogo";
+import { IconButton } from "./button";
+import UserAvatar from "./user/UserAvatar";
+
+import "./AppBar.css";
+
+function AppBarNavLink({
+  link,
+  className,
+  label,
+  onClick,
+  children,
+}: {
+  link: string;
+  className?: string;
+  label?: I18nText;
+  onClick?: () => void;
+  children?: React.ReactNode;
+}) {
+  if (label != null && children != null) {
+    throw new Error("AppBarNavLink: label and children cannot be both set");
+  }
+
+  const c = useC();
+
+  return (
+    <NavLink
+      to={link}
+      className={({ isActive }) => classnames(className, isActive && "active")}
+      onClick={onClick}
+    >
+      {children != null ? children : c(label)}
+    </NavLink>
+  );
+}
+
+export default function AppBar() {
+  const isMobile = useMobile();
+
+  const [isCollapse, setIsCollapse] = useState<boolean>(true);
+  const collapse = isMobile ? () => setIsCollapse(true) : undefined;
+  const toggleCollapse = () => setIsCollapse(!isCollapse);
+
+  const user = useUser();
+  const hasAdministrationPermission = user && user.hasAdministrationPermission;
+
+  return (
+    <nav
+      className={classnames(
+        isMobile ? "mobile" : "desktop",
+        "app-bar",
+        isCollapse && "collapse",
+      )}
+    >
+      <Link to="/" className="app-bar-brand" onClick={collapse}>
+        <TimelineLogo className="app-bar-brand-icon" />
+        Timeline
+      </Link>
+
+      <div className="app-bar-link-area">
+        <AppBarNavLink
+          link="/settings"
+          label="nav.settings"
+          onClick={collapse}
+        />
+        <AppBarNavLink link="/about" label="nav.about" onClick={collapse} />
+        {hasAdministrationPermission && (
+          <AppBarNavLink
+            link="/admin"
+            label="nav.administration"
+            onClick={collapse}
+          />
+        )}
+      </div>
+
+      <div className="app-bar-user-area">
+        {user != null ? (
+          <AppBarNavLink link="/" className="app-bar-avatar" onClick={collapse}>
+            <UserAvatar username={user.username} />
+          </AppBarNavLink>
+        ) : (
+          <AppBarNavLink link="/login" label="nav.login" onClick={collapse} />
+        )}
+      </div>
+
+      {isMobile && (
+        <IconButton icon="list" className="toggler" onClick={toggleCollapse} />
+      )}
+    </nav>
+  );
+}
diff --git a/FrontEnd/src/components/BlobImage.tsx b/FrontEnd/src/components/BlobImage.tsx
new file mode 100644
index 00000000..259c2210
--- /dev/null
+++ b/FrontEnd/src/components/BlobImage.tsx
@@ -0,0 +1,26 @@
+import { ComponentPropsWithoutRef, useState, useEffect } from "react";
+
+type BlobImageProps = Omit<ComponentPropsWithoutRef<"img">, "src"> & {
+  imgRef?: React.Ref<HTMLImageElement>;
+  src?: Blob | string | null;
+};
+
+export default function BlobImage(props: BlobImageProps) {
+  const { imgRef, src, ...otherProps } = props;
+
+  const [url, setUrl] = useState<string | null | undefined>(undefined);
+
+  useEffect(() => {
+    if (src instanceof Blob) {
+      const url = URL.createObjectURL(src);
+      setUrl(url);
+      return () => {
+        URL.revokeObjectURL(url);
+      };
+    } else {
+      setUrl(src);
+    }
+  }, [src]);
+
+  return <img ref={imgRef} {...otherProps} src={url ?? undefined} />;
+}
diff --git a/FrontEnd/src/components/Card.css b/FrontEnd/src/components/Card.css
new file mode 100644
index 00000000..6d655eb9
--- /dev/null
+++ b/FrontEnd/src/components/Card.css
@@ -0,0 +1,20 @@
+.cru-card {
+  border-radius: var(--cru-card-border-radius);
+  transition: all 0.3s;
+}
+
+.cru-card-background-none {
+  background-color: transparent;
+}
+
+.cru-card-background-solid {
+  background-color: var(--cru-background-color);
+}
+
+.cru-card-background-grayscale {
+  background-color: var(--cru-container-background-color);
+}
+
+.cru-card-border-color {
+  border: 2px solid var(--cru-card-border-color);
+}
diff --git a/FrontEnd/src/components/Card.tsx b/FrontEnd/src/components/Card.tsx
new file mode 100644
index 00000000..a8f0d3cc
--- /dev/null
+++ b/FrontEnd/src/components/Card.tsx
@@ -0,0 +1,38 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+import { ThemeColor } from "./common";
+import "./Card.css";
+
+interface CardProps extends ComponentPropsWithoutRef<"div"> {
+  containerRef?: Ref<HTMLDivElement>;
+  color?: ThemeColor;
+  border?: "color" | "none";
+  background?: "color" | "solid" | "grayscale" | "none";
+}
+
+export default function Card({
+  color,
+  background,
+  border,
+  className,
+  children,
+  containerRef,
+  ...otherProps
+}: CardProps) {
+  return (
+    <div
+      ref={containerRef}
+      className={classNames(
+        "cru-card",
+        `cru-card-${color ?? "primary"}`,
+        `cru-card-border-${border ?? "color"}`,
+        `cru-card-background-${background ?? "solid"}`,
+        className,
+      )}
+      {...otherProps}
+    >
+      {children}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/Icon.css b/FrontEnd/src/components/Icon.css
new file mode 100644
index 00000000..fe980d7b
--- /dev/null
+++ b/FrontEnd/src/components/Icon.css
@@ -0,0 +1,3 @@
+.cru-icon {
+  font-size: 1.4rem;
+}
diff --git a/FrontEnd/src/components/Icon.tsx b/FrontEnd/src/components/Icon.tsx
new file mode 100644
index 00000000..2ac3a7ca
--- /dev/null
+++ b/FrontEnd/src/components/Icon.tsx
@@ -0,0 +1,30 @@
+import { ComponentPropsWithoutRef } from "react";
+import classNames from "classnames";
+
+import { ThemeColor } from "./common";
+
+import "./Icon.css";
+
+interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
+  icon: string;
+  color?: ThemeColor | "on-surface";
+  size?: string | number;
+}
+
+export default function Icon(props: IconButtonProps) {
+  const { icon, color, size, style, className, ...otherProps } = props;
+
+  const colorName = color === "on-surface" ? "surface-on" : color;
+
+  return (
+    <i
+      style={size != null ? { ...style, fontSize: size } : style}
+      className={classNames(
+        colorName && `cru-${colorName}`,
+        `bi-${icon} cru-icon`,
+        className,
+      )}
+      {...otherProps}
+    />
+  );
+}
diff --git a/FrontEnd/src/components/ImageCropper.css b/FrontEnd/src/components/ImageCropper.css
new file mode 100644
index 00000000..2c4d0a8c
--- /dev/null
+++ b/FrontEnd/src/components/ImageCropper.css
@@ -0,0 +1,38 @@
+.image-cropper-container {
+  position: relative;
+  box-sizing: border-box;
+  user-select: none;
+}
+
+.image-cropper-container img {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.image-cropper-mask-container {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+}
+
+.image-cropper-mask {
+  position: absolute;
+  box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
+  touch-action: none;
+}
+
+.image-cropper-handler {
+  position: absolute;
+  width: 26px;
+  height: 26px;
+  border: black solid 2px;
+  border-radius: 50%;
+  background: white;
+  touch-action: none;
+}
diff --git a/FrontEnd/src/components/ImageCropper.tsx b/FrontEnd/src/components/ImageCropper.tsx
new file mode 100644
index 00000000..f23994e2
--- /dev/null
+++ b/FrontEnd/src/components/ImageCropper.tsx
@@ -0,0 +1,312 @@
+import * as React from "react";
+import classnames from "classnames";
+
+import { UiLogicError } from "~src/common";
+
+import "./ImageCropper.css";
+import BlobImage from "./BlobImage";
+
+export interface Clip {
+  left: number;
+  top: number;
+  width: number;
+}
+
+interface NormailizedClip extends Clip {
+  height: number;
+}
+
+interface ImageInfo {
+  width: number;
+  height: number;
+  landscape: boolean;
+  ratio: number;
+  maxClipWidth: number;
+  maxClipHeight: number;
+}
+
+interface ImageCropperSavedState {
+  clip: NormailizedClip;
+  x: number;
+  y: number;
+  pointerId: number;
+}
+
+export interface ImageCropperProps {
+  clip: Clip | null;
+  image: string | Blob;
+  onChange: (clip: Clip) => void;
+  imageElementCallback?: (element: HTMLImageElement | null) => void;
+  className?: string;
+}
+
+const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
+  const { clip, image, onChange, imageElementCallback, className } = props;
+
+  const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>(
+    null,
+  );
+  const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null);
+
+  const normalizeClip = (c: Clip | null | undefined): NormailizedClip => {
+    if (c == null) {
+      return { left: 0, top: 0, width: 0, height: 0 };
+    }
+
+    return {
+      left: c.left || 0,
+      top: c.top || 0,
+      width: c.width || 0,
+      height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0,
+    };
+  };
+
+  const c = normalizeClip(clip);
+
+  const imgElementRef = React.useRef<HTMLImageElement | null>(null);
+
+  const onImageRef = React.useCallback(
+    (e: HTMLImageElement | null) => {
+      imgElementRef.current = e;
+      if (imageElementCallback != null && e == null) {
+        imageElementCallback(null);
+      }
+    },
+    [imageElementCallback],
+  );
+
+  const onImageLoad = React.useCallback(
+    (e: React.SyntheticEvent<HTMLImageElement>) => {
+      const img = e.currentTarget;
+      const landscape = img.naturalWidth >= img.naturalHeight;
+
+      const info = {
+        width: img.naturalWidth,
+        height: img.naturalHeight,
+        landscape,
+        ratio: img.naturalHeight / img.naturalWidth,
+        maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1,
+        maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight,
+      };
+      setImageInfo(info);
+      onChange({ left: 0, top: 0, width: info.maxClipWidth });
+      if (imageElementCallback != null) {
+        imageElementCallback(img);
+      }
+    },
+    [onChange, imageElementCallback],
+  );
+
+  const onPointerDown = React.useCallback(
+    (e: React.PointerEvent) => {
+      if (oldState != null) return;
+      e.currentTarget.setPointerCapture(e.pointerId);
+      setOldState({
+        x: e.clientX,
+        y: e.clientY,
+        clip: c,
+        pointerId: e.pointerId,
+      });
+    },
+    [oldState, c],
+  );
+
+  const onPointerUp = React.useCallback(
+    (e: React.PointerEvent) => {
+      if (oldState == null || oldState.pointerId !== e.pointerId) return;
+      e.currentTarget.releasePointerCapture(e.pointerId);
+      setOldState(null);
+    },
+    [oldState],
+  );
+
+  const onPointerMove = React.useCallback(
+    (e: React.PointerEvent) => {
+      if (oldState == null) return;
+
+      const oldClip = oldState.clip;
+
+      const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
+
+      const { current: imgElement } = imgElementRef;
+
+      if (imgElement == null) throw new UiLogicError("Image element is null.");
+
+      const moveRatio = {
+        x: movement.x / imgElement.width,
+        y: movement.y / imgElement.height,
+      };
+
+      const newRatio = {
+        x: oldClip.left + moveRatio.x,
+        y: oldClip.top + moveRatio.y,
+      };
+      if (newRatio.x < 0) {
+        newRatio.x = 0;
+      } else if (newRatio.x > 1 - oldClip.width) {
+        newRatio.x = 1 - oldClip.width;
+      }
+      if (newRatio.y < 0) {
+        newRatio.y = 0;
+      } else if (newRatio.y > 1 - oldClip.height) {
+        newRatio.y = 1 - oldClip.height;
+      }
+
+      onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width });
+    },
+    [oldState, onChange],
+  );
+
+  const onHandlerPointerMove = React.useCallback(
+    (e: React.PointerEvent) => {
+      if (oldState == null) return;
+
+      const oldClip = oldState.clip;
+
+      const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
+
+      const ratio = imageInfo == null ? 1 : imageInfo.ratio;
+
+      const { current: imgElement } = imgElementRef;
+
+      if (imgElement == null) throw new UiLogicError("Image element is null.");
+
+      const moveRatio = {
+        x: movement.x / imgElement.width,
+        y: movement.x / imgElement.width / ratio,
+      };
+
+      const newRatio = {
+        x: oldClip.width + moveRatio.x,
+        y: oldClip.height + moveRatio.y,
+      };
+
+      const maxRatio = {
+        x: Math.min(1 - oldClip.left, newRatio.x),
+        y: Math.min(1 - oldClip.top, newRatio.y),
+      };
+
+      const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio);
+
+      let newWidth;
+      if (newRatio.x < 0) {
+        newWidth = 0;
+      } else if (newRatio.x > maxWidthRatio) {
+        newWidth = maxWidthRatio;
+      } else {
+        newWidth = newRatio.x;
+      }
+
+      onChange({ left: oldClip.left, top: oldClip.top, width: newWidth });
+    },
+    [imageInfo, oldState, onChange],
+  );
+
+  const toPercentage = (n: number): string => `${n}%`;
+
+  // fuck!!! I just can't find a better way to implement this in pure css
+  const containerStyle: React.CSSProperties = (() => {
+    if (imageInfo == null) {
+      return { width: "100%", paddingTop: "100%", height: 0 };
+    } else {
+      if (imageInfo.ratio > 1) {
+        return {
+          width: toPercentage(100 / imageInfo.ratio),
+          paddingTop: "100%",
+          height: 0,
+        };
+      } else {
+        return {
+          width: "100%",
+          paddingTop: toPercentage(100 * imageInfo.ratio),
+          height: 0,
+        };
+      }
+    }
+  })();
+
+  return (
+    <div
+      className={classnames("image-cropper-container", className)}
+      style={containerStyle}
+    >
+      <BlobImage
+        imgRef={onImageRef}
+        src={image}
+        onLoad={onImageLoad}
+        alt="to crop"
+      />
+      <div className="image-cropper-mask-container">
+        <div
+          className="image-cropper-mask"
+          style={{
+            left: toPercentage(c.left * 100),
+            top: toPercentage(c.top * 100),
+            width: toPercentage(c.width * 100),
+            height: toPercentage(c.height * 100),
+          }}
+          onPointerMove={onPointerMove}
+          onPointerDown={onPointerDown}
+          onPointerUp={onPointerUp}
+        />
+      </div>
+      <div
+        className="image-cropper-handler"
+        style={{
+          left: `calc(${(c.left + c.width) * 100}% - 15px)`,
+          top: `calc(${(c.top + c.height) * 100}% - 15px)`,
+        }}
+        onPointerMove={onHandlerPointerMove}
+        onPointerDown={onPointerDown}
+        onPointerUp={onPointerUp}
+      />
+    </div>
+  );
+};
+
+export default ImageCropper;
+
+export function applyClipToImage(
+  image: HTMLImageElement,
+  clip: Clip,
+  mimeType: string,
+): Promise<Blob> {
+  return new Promise((resolve, reject) => {
+    const naturalSize = {
+      width: image.naturalWidth,
+      height: image.naturalHeight,
+    };
+    const clipArea = {
+      x: naturalSize.width * clip.left,
+      y: naturalSize.height * clip.top,
+      length: naturalSize.width * clip.width,
+    };
+
+    const canvas = document.createElement("canvas");
+    canvas.width = clipArea.length;
+    canvas.height = clipArea.length;
+    const context = canvas.getContext("2d");
+
+    if (context == null) throw new Error("Failed to create context.");
+
+    context.drawImage(
+      image,
+      clipArea.x,
+      clipArea.y,
+      clipArea.length,
+      clipArea.length,
+      0,
+      0,
+      clipArea.length,
+      clipArea.length,
+    );
+
+    canvas.toBlob((blob) => {
+      if (blob == null) {
+        reject(new Error("canvas.toBlob returns null"));
+      } else {
+        resolve(blob);
+      }
+    }, mimeType);
+  });
+}
diff --git a/FrontEnd/src/components/LoadFailReload.tsx b/FrontEnd/src/components/LoadFailReload.tsx
new file mode 100644
index 00000000..81ba1f67
--- /dev/null
+++ b/FrontEnd/src/components/LoadFailReload.tsx
@@ -0,0 +1,37 @@
+import * as React from "react";
+import { Trans } from "react-i18next";
+
+export interface LoadFailReloadProps {
+  className?: string;
+  style?: React.CSSProperties;
+  onReload: () => void;
+}
+
+const LoadFailReload: React.FC<LoadFailReloadProps> = ({
+  onReload,
+  className,
+  style,
+}) => {
+  return (
+    <Trans
+      i18nKey="loadFailReload"
+      parent="div"
+      className={className}
+      style={style}
+    >
+      0
+      <a
+        href="#"
+        onClick={(e) => {
+          onReload();
+          e.preventDefault();
+        }}
+      >
+        1
+      </a>
+      2
+    </Trans>
+  );
+};
+
+export default LoadFailReload;
diff --git a/FrontEnd/src/components/LoadingPage.tsx b/FrontEnd/src/components/LoadingPage.tsx
new file mode 100644
index 00000000..35ee1aa8
--- /dev/null
+++ b/FrontEnd/src/components/LoadingPage.tsx
@@ -0,0 +1,13 @@
+import * as React from "react";
+
+import Spinner from "./Spinner";
+
+const LoadingPage: React.FC = () => {
+  return (
+    <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center">
+      <Spinner />
+    </div>
+  );
+};
+
+export default LoadingPage;
diff --git a/FrontEnd/src/components/Page.tsx b/FrontEnd/src/components/Page.tsx
new file mode 100644
index 00000000..86fdb2f5
--- /dev/null
+++ b/FrontEnd/src/components/Page.tsx
@@ -0,0 +1,15 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+interface PageProps extends ComponentPropsWithoutRef<"div"> {
+  noTopPadding?: boolean;
+  pageRef?: Ref<HTMLDivElement>;
+}
+
+export default function Page({ noTopPadding, pageRef, className, children }: PageProps) {
+  return (
+    <div ref={pageRef} className={classNames(className, "cru-page", noTopPadding && "cru-page-no-top-padding")}>
+      {children}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/SearchInput.css b/FrontEnd/src/components/SearchInput.css
new file mode 100644
index 00000000..f0503016
--- /dev/null
+++ b/FrontEnd/src/components/SearchInput.css
@@ -0,0 +1,8 @@
+.cru-search-input {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.cru-search-input-input {
+  width: 100%;
+}
diff --git a/FrontEnd/src/components/SearchInput.tsx b/FrontEnd/src/components/SearchInput.tsx
new file mode 100644
index 00000000..e3216b86
--- /dev/null
+++ b/FrontEnd/src/components/SearchInput.tsx
@@ -0,0 +1,50 @@
+import classNames from "classnames";
+
+import { useC, Text } from "./common";
+import LoadingButton from "./button/LoadingButton";
+
+import "./SearchInput.css";
+
+interface SearchInputProps {
+  value: string;
+  onChange: (value: string) => void;
+  onButtonClick: () => void;
+  loading?: boolean;
+  className?: string;
+  buttonText?: Text;
+}
+
+export default function SearchInput({
+  value,
+  onChange,
+  onButtonClick,
+  loading,
+  className,
+  buttonText,
+}: SearchInputProps) {
+  const c = useC();
+
+  return (
+    <div className={classNames("cru-search-input", className)}>
+      <input
+        type="text"
+        className="cru-search-input-input"
+        value={value}
+        onChange={(event) => {
+          const { value } = event.currentTarget;
+          onChange(value);
+        }}
+        onKeyDown={(event) => {
+          if (event.key === "Enter") {
+            onButtonClick();
+            event.preventDefault();
+          }
+        }}
+      />
+
+      <LoadingButton loading={loading} onClick={onButtonClick}>
+        {c(buttonText ?? "search")}
+      </LoadingButton>
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/Skeleton.css b/FrontEnd/src/components/Skeleton.css
new file mode 100644
index 00000000..a571eead
--- /dev/null
+++ b/FrontEnd/src/components/Skeleton.css
@@ -0,0 +1,14 @@
+.cru-skeleton {
+  padding: 0 1em;
+}
+
+.cru-skeleton-line {
+  height: 1em;
+  background-color: hsl(0, 0%, 90%);
+  margin: 0.7em 0;
+  border-radius: 0.2em;
+}
+
+.cru-skeleton-line.last {
+  width: 50%;
+}
diff --git a/FrontEnd/src/components/Skeleton.tsx b/FrontEnd/src/components/Skeleton.tsx
new file mode 100644
index 00000000..3b149db9
--- /dev/null
+++ b/FrontEnd/src/components/Skeleton.tsx
@@ -0,0 +1,32 @@
+import * as React from "react";
+import classnames from "classnames";
+import range from "lodash/range";
+
+import "./Skeleton.css";
+
+export interface SkeletonProps {
+  lineNumber?: number;
+  className?: string;
+  style?: React.CSSProperties;
+}
+
+const Skeleton: React.FC<SkeletonProps> = (props) => {
+  const { lineNumber: lineNumberProps, className, style } = props;
+  const lineNumber = lineNumberProps ?? 3;
+
+  return (
+    <div className={classnames(className, "cru-skeleton")} style={style}>
+      {range(lineNumber).map((i) => (
+        <div
+          key={i}
+          className={classnames(
+            "cru-skeleton-line",
+            i === lineNumber - 1 && "last"
+          )}
+        />
+      ))}
+    </div>
+  );
+};
+
+export default Skeleton;
diff --git a/FrontEnd/src/components/Spinner.css b/FrontEnd/src/components/Spinner.css
new file mode 100644
index 00000000..a1de68d2
--- /dev/null
+++ b/FrontEnd/src/components/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/components/Spinner.tsx b/FrontEnd/src/components/Spinner.tsx
new file mode 100644
index 00000000..ec0c2c35
--- /dev/null
+++ b/FrontEnd/src/components/Spinner.tsx
@@ -0,0 +1,36 @@
+import { CSSProperties } from "react";
+import classnames from "classnames";
+
+import { ThemeColor } from "./common";
+
+import "./Spinner.css";
+
+export interface SpinnerProps {
+  size?: "sm" | "md" | "lg" | number | string;
+  color?: ThemeColor;
+  className?: string;
+  style?: CSSProperties;
+}
+
+export default function Spinner(props: SpinnerProps) {
+  const { size, color, className, style } = props;
+  const calculatedSize =
+    size === "sm"
+      ? "18px"
+      : size === "md"
+      ? "30px"
+      : size === "lg"
+      ? "42px"
+      : typeof size === "number"
+      ? size
+      : size == null
+      ? "20px"
+      : size;
+
+  return (
+    <span
+      className={classnames("cru-spinner", color && `cru-${color}`, className)}
+      style={{ width: calculatedSize, height: calculatedSize, ...style }}
+    />
+  );
+}
diff --git a/FrontEnd/src/components/TimelineLogo.tsx b/FrontEnd/src/components/TimelineLogo.tsx
new file mode 100644
index 00000000..e06ed0f5
--- /dev/null
+++ b/FrontEnd/src/components/TimelineLogo.tsx
@@ -0,0 +1,27 @@
+import { SVGAttributes } from "react";
+import * as React from "react";
+
+export interface TimelineLogoProps extends SVGAttributes<SVGElement> {
+  color?: string;
+}
+
+const TimelineLogo: React.FC<TimelineLogoProps> = (props) => {
+  const { color, ...forwardProps } = props;
+  const coercedColor = color ?? "currentcolor";
+  return (
+    <svg
+      className={props.className}
+      viewBox="0 0 100 100"
+      fill="none"
+      strokeWidth="12"
+      stroke={coercedColor}
+      {...forwardProps}
+    >
+      <line x1="50" y1="0" x2="50" y2="25" />
+      <circle cx="50" cy="50" r="22" />
+      <line x1="50" y1="75" x2="50" y2="100" />
+    </svg>
+  );
+};
+
+export default TimelineLogo;
diff --git a/FrontEnd/src/components/alert/AlertHost.tsx b/FrontEnd/src/components/alert/AlertHost.tsx
new file mode 100644
index 00000000..b234ac03
--- /dev/null
+++ b/FrontEnd/src/components/alert/AlertHost.tsx
@@ -0,0 +1,113 @@
+import * as React from "react";
+import without from "lodash/without";
+import { useTranslation } from "react-i18next";
+import classNames from "classnames";
+
+import { alertService, AlertInfoEx, AlertInfo } from "~src/services/alert";
+import { convertI18nText } from "~src/common";
+
+import IconButton from "../button/IconButton";
+
+import "./alert.css";
+
+interface AutoCloseAlertProps {
+  alert: AlertInfo;
+  close: () => void;
+}
+
+export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
+  const { alert, close } = props;
+  const { dismissTime } = alert;
+
+  const { t } = useTranslation();
+
+  const timerTag = React.useRef<number | null>(null);
+  const closeHandler = React.useRef<(() => void) | null>(null);
+
+  React.useEffect(() => {
+    closeHandler.current = close;
+  }, [close]);
+
+  React.useEffect(() => {
+    const tag =
+      dismissTime === "never"
+        ? null
+        : typeof dismissTime === "number"
+        ? window.setTimeout(() => closeHandler.current?.(), dismissTime)
+        : window.setTimeout(() => closeHandler.current?.(), 5000);
+    timerTag.current = tag;
+    return () => {
+      if (tag != null) {
+        window.clearTimeout(tag);
+      }
+    };
+  }, [dismissTime]);
+
+  const cancelTimer = (): void => {
+    const { current: tag } = timerTag;
+    if (tag != null) {
+      window.clearTimeout(tag);
+    }
+  };
+
+  return (
+    <div
+      className={classNames(
+        "m-3 cru-alert",
+        "cru-" + (alert.type ?? "primary")
+      )}
+      onClick={cancelTimer}
+    >
+      <div className="cru-alert-content">
+        {(() => {
+          const { message, customMessage } = alert;
+          if (customMessage != null) {
+            return customMessage;
+          } else {
+            return convertI18nText(message, t);
+          }
+        })()}
+      </div>
+      <div className="cru-alert-close-button-container">
+        <IconButton
+          icon="x"
+          className="cru-alert-close-button"
+          onClick={close}
+        />
+      </div>
+    </div>
+  );
+};
+
+const AlertHost: React.FC = () => {
+  const [alerts, setAlerts] = React.useState<AlertInfoEx[]>([]);
+
+  React.useEffect(() => {
+    const consume = (alert: AlertInfoEx): void => {
+      setAlerts((old) => [...old, alert]);
+    };
+
+    alertService.registerConsumer(consume);
+    return () => {
+      alertService.unregisterConsumer(consume);
+    };
+  }, []);
+
+  return (
+    <div className="alert-container">
+      {alerts.map((alert) => {
+        return (
+          <AutoCloseAlert
+            key={alert.id}
+            alert={alert}
+            close={() => {
+              setAlerts((old) => without(old, alert));
+            }}
+          />
+        );
+      })}
+    </div>
+  );
+};
+
+export default AlertHost;
diff --git a/FrontEnd/src/components/alert/alert.css b/FrontEnd/src/components/alert/alert.css
new file mode 100644
index 00000000..54c2b87f
--- /dev/null
+++ b/FrontEnd/src/components/alert/alert.css
@@ -0,0 +1,33 @@
+.alert-container {
+  position: fixed;
+  z-index: 1040;
+}
+
+.cru-alert {
+  border-radius: 5px;
+  border: var(--cru-key-color) 1px solid;
+  color: var(--cru-key-t-color);
+  background-color: var(--cru-key-b1-color);
+
+  display: flex;
+  overflow: hidden;
+}
+
+.cru-alert-content {
+  padding: 0.5em 2em;
+}
+
+.cru-alert-close-button-container {
+  flex-shrink: 0;
+  margin-left: auto;
+  width: 2em;
+  text-align: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: var(--cru-key-t-color);
+}
+
+.cru-alert-close-button {
+  color: var(--cru-key-color);
+}
diff --git a/FrontEnd/src/components/breakpoints.ts b/FrontEnd/src/components/breakpoints.ts
new file mode 100644
index 00000000..fb281610
--- /dev/null
+++ b/FrontEnd/src/components/breakpoints.ts
@@ -0,0 +1,3 @@
+export const breakpoints = {
+  sm: 576,
+} as const;
diff --git a/FrontEnd/src/components/button/Button.css b/FrontEnd/src/components/button/Button.css
new file mode 100644
index 00000000..1da70f0e
--- /dev/null
+++ b/FrontEnd/src/components/button/Button.css
@@ -0,0 +1,64 @@
+.cru-button {
+  font-size: 1rem;
+  padding: 0.4em 0.8em;
+  transition: all 0.3s;
+  border-radius: 0.2em;
+  border: 1px solid;
+  cursor: pointer;
+}
+
+.cru-button:not(.outline) {
+  color: var(--cru-push-button-text-color);
+  background-color: var(--cru-clickable-normal-color);
+  border-color: var(--cru-clickable-normal-color);
+}
+
+.cru-button:not(.outline):hover {
+  background-color: var(--cru-clickable-hover-color);
+  border-color: var(--cru-clickable-hover-color);
+}
+
+.cru-button:not(.outline):focus {
+  background-color: var(--cru-clickable-focus-color);
+  border-color: var(--cru-clickable-focus-color);
+}
+
+.cru-button:not(.outline):active {
+  background-color: var(--cru-clickable-active-color);
+  border-color: var(--cru-clickable-active-color);
+}
+
+.cru-button:not(.outline):disabled {
+  color: var(--cru-push-button-disabled-text-color);
+  background-color: var(--cru-push-button-disabled-color);
+  border-color: var(--cru-push-button-disabled-color);
+  cursor: auto;
+}
+
+
+.cru-button.outline {
+  color: var(--cru-clickable-normal-color);
+  border-color: var(--cru-clickable-normal-color);
+  background-color: transparent;
+}
+
+.cru-button.outline:hover {
+  color: var(--cru-clickable-hover-color);
+  border-color: var(--cru-clickable-hover-color);
+}
+
+.cru-button.outline:focus {
+  color: var(--cru-clickable-focus-color);
+  border-color: var(--cru-clickable-focus-color);
+}
+
+.cru-button.outline:active {
+  color: var(--cru-clickable-active-color);
+  border-color: var(--cru-clickable-active-color);
+}
+
+.cru-button.outline:disabled {
+  color: var(--cru-clickable-disabled-color);
+  border-color: var(--cru-clickable-disabled-color);
+  cursor: auto;
+}
diff --git a/FrontEnd/src/components/button/Button.tsx b/FrontEnd/src/components/button/Button.tsx
new file mode 100644
index 00000000..6c38e130
--- /dev/null
+++ b/FrontEnd/src/components/button/Button.tsx
@@ -0,0 +1,46 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+import { Text, useC, ThemeColor } from "../common";
+
+import "./Button.css";
+
+interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
+  color?: ThemeColor;
+  text?: Text;
+  outline?: boolean;
+  buttonRef?: Ref<HTMLButtonElement> | null;
+}
+
+export default function Button(props: ButtonProps) {
+  const {
+    buttonRef,
+    color,
+    text,
+    outline,
+    className,
+    children,
+    ...otherProps
+  } = props;
+
+  if (text != null && children != null) {
+    console.warn("You can't set both text and children props.");
+  }
+
+  const c = useC();
+
+  return (
+    <button
+      ref={buttonRef}
+      className={classNames(
+        "cru-button",
+        `cru-clickable-${color ?? "primary"}`,
+        outline && "outline",
+        className,
+      )}
+      {...otherProps}
+    >
+      {text != null ? c(text) : children}
+    </button>
+  );
+}
diff --git a/FrontEnd/src/components/button/ButtonRow.css b/FrontEnd/src/components/button/ButtonRow.css
new file mode 100644
index 00000000..e69de29b
diff --git a/FrontEnd/src/components/button/ButtonRow.tsx b/FrontEnd/src/components/button/ButtonRow.tsx
new file mode 100644
index 00000000..eea60cc4
--- /dev/null
+++ b/FrontEnd/src/components/button/ButtonRow.tsx
@@ -0,0 +1,62 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+import Button from "./Button";
+import FlatButton from "./FlatButton";
+import IconButton from "./IconButton";
+import LoadingButton from "./LoadingButton";
+
+import "./ButtonRow.css";
+
+type ButtonRowButton = (
+  | {
+      type: "normal";
+      props: ComponentPropsWithoutRef<typeof Button>;
+    }
+  | {
+      type: "flat";
+      props: ComponentPropsWithoutRef<typeof FlatButton>;
+    }
+  | {
+      type: "icon";
+      props: ComponentPropsWithoutRef<typeof IconButton>;
+    }
+  | { type: "loading"; props: ComponentPropsWithoutRef<typeof LoadingButton> }
+) & { key: string | number };
+
+interface ButtonRowProps {
+  className?: string;
+  containerRef?: Ref<HTMLDivElement>;
+  buttons: ButtonRowButton[];
+  buttonsClassName?: string;
+}
+
+export default function ButtonRow({
+  className,
+  containerRef,
+  buttons,
+  buttonsClassName,
+}: ButtonRowProps) {
+  return (
+    <div ref={containerRef} className={classNames("cru-button-row", className)}>
+      {buttons.map((button) => {
+        const { type, key, props } = button;
+        const newClassName = classNames(props.className, buttonsClassName);
+        switch (type) {
+          case "normal":
+            return <Button key={key} {...props} className={newClassName} />;
+          case "flat":
+            return <FlatButton key={key} {...props} className={newClassName} />;
+          case "icon":
+            return <IconButton key={key} {...props} className={newClassName} />;
+          case "loading":
+            return (
+              <LoadingButton key={key} {...props} className={newClassName} />
+            );
+          default:
+            throw new Error();
+        }
+      })}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/button/ButtonRowV2.tsx b/FrontEnd/src/components/button/ButtonRowV2.tsx
new file mode 100644
index 00000000..3467ad52
--- /dev/null
+++ b/FrontEnd/src/components/button/ButtonRowV2.tsx
@@ -0,0 +1,143 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+import Button from "./Button";
+import FlatButton from "./FlatButton";
+import IconButton from "./IconButton";
+import LoadingButton from "./LoadingButton";
+
+import "./ButtonRow.css";
+import { Text, ThemeColor } from "../common";
+
+interface ButtonRowV2ButtonBase {
+  key: string | number;
+  action?: "primary" | "secondary";
+  color?: ThemeColor;
+  disabled?: boolean;
+  onClick?: () => void;
+}
+
+interface ButtonRowV2ButtonWithNoType extends ButtonRowV2ButtonBase {
+  type?: undefined | null;
+  text: Text;
+  outline?: boolean;
+  props?: ComponentPropsWithoutRef<typeof Button>;
+}
+
+interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase {
+  type: "normal";
+  text: Text;
+  outline?: boolean;
+  props?: ComponentPropsWithoutRef<typeof Button>;
+}
+
+interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase {
+  type: "flat";
+  text: Text;
+  props?: ComponentPropsWithoutRef<typeof FlatButton>;
+}
+
+interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase {
+  type: "icon";
+  icon: string;
+  props?: ComponentPropsWithoutRef<typeof IconButton>;
+}
+
+interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase {
+  type: "loading";
+  text: Text;
+  loading?: boolean;
+  props?: ComponentPropsWithoutRef<typeof LoadingButton>;
+}
+
+type ButtonRowV2Button =
+  | ButtonRowV2ButtonWithNoType
+  | ButtonRowV2NormalButton
+  | ButtonRowV2FlatButton
+  | ButtonRowV2IconButton
+  | ButtonRowV2LoadingButton;
+
+interface ButtonRowV2Props {
+  className?: string;
+  containerRef?: Ref<HTMLDivElement>;
+  buttons: ButtonRowV2Button[];
+  buttonsClassName?: string;
+}
+
+export default function ButtonRowV2({
+  className,
+  containerRef,
+  buttons,
+  buttonsClassName,
+}: ButtonRowV2Props) {
+  return (
+    <div ref={containerRef} className={classNames("cru-button-row", className)}>
+      {buttons.map((button) => {
+        const { key, action, color, disabled, onClick } = button;
+
+        const realAction = action ?? "primary";
+        const realColor =
+          color ?? (realAction === "primary" ? "primary" : "secondary");
+
+        const commonProps = { key, color: realColor, disabled, onClick };
+        const newClassName = classNames(
+          button.props?.className,
+          buttonsClassName,
+        );
+
+        switch (button.type) {
+          case null:
+          case undefined:
+          case "normal": {
+            const { text, outline, props } = button;
+            return (
+              <Button
+                {...commonProps}
+                text={text}
+                outline={outline ?? realAction !== "primary"}
+                {...props}
+                className={newClassName}
+              />
+            );
+          }
+          case "flat": {
+            const { text, props } = button;
+            return (
+              <FlatButton
+                {...commonProps}
+                text={text}
+                {...props}
+                className={newClassName}
+              />
+            );
+          }
+          case "icon": {
+            const { icon, props } = button;
+            return (
+              <IconButton
+                {...commonProps}
+                icon={icon}
+                {...props}
+                className={newClassName}
+              />
+            );
+          }
+          case "loading": {
+            const { text, loading, props } = button;
+            return (
+              <LoadingButton
+                {...commonProps}
+                text={text}
+                loading={loading}
+                {...props}
+                className={newClassName}
+              />
+            );
+          }
+          default:
+            throw new Error();
+        }
+      })}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/button/FlatButton.css b/FrontEnd/src/components/button/FlatButton.css
new file mode 100644
index 00000000..2050946c
--- /dev/null
+++ b/FrontEnd/src/components/button/FlatButton.css
@@ -0,0 +1,27 @@
+.cru-flat-button {
+  font-size: 1rem;
+  padding: 0.4em 0.8em;
+  transition: all 0.5s;
+  border-radius: 0.2em;
+  background-color: var(--cru-clickable-grayscale-normal-color);
+  border: 1px none;
+  color: var(--cru-clickable-normal-color);
+  cursor: pointer;
+}
+
+.cru-flat-button:hover {
+  background-color: var(--cru-clickable-grayscale-hover-color);
+}
+
+.cru-flat-button:focus {
+  background-color: var(--cru-clickable-grayscale-focus-color);
+}
+
+.cru-flat-button:active {
+  background-color: var(--cru-clickable-grayscale-active-color);
+}
+
+.cru-flat-button:disabled {
+  color: var(--cru-clickable-disabled-color);
+  cursor: auto;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/button/FlatButton.tsx b/FrontEnd/src/components/button/FlatButton.tsx
new file mode 100644
index 00000000..9f074dd6
--- /dev/null
+++ b/FrontEnd/src/components/button/FlatButton.tsx
@@ -0,0 +1,36 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+import { Text, useC, ThemeColor } from "../common";
+
+import "./FlatButton.css";
+
+interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> {
+  color?: ThemeColor;
+  text?: Text;
+  buttonRef?: Ref<HTMLButtonElement> | null;
+}
+
+export default function FlatButton(props: FlatButtonProps) {
+  const { color, text, className, children, buttonRef, ...otherProps } = props;
+
+  if (text != null && children != null) {
+    console.warn("You can't set both text and children props.");
+  }
+
+  const c = useC();
+
+  return (
+    <button
+      ref={buttonRef}
+      className={classNames(
+        "cru-flat-button",
+        `cru-clickable-${color ?? "primary"}`,
+        className,
+      )}
+      {...otherProps}
+    >
+      {text != null ? c(text) : children}
+    </button>
+  );
+}
diff --git a/FrontEnd/src/components/button/IconButton.css b/FrontEnd/src/components/button/IconButton.css
new file mode 100644
index 00000000..a3747201
--- /dev/null
+++ b/FrontEnd/src/components/button/IconButton.css
@@ -0,0 +1,30 @@
+.cru-icon-button {
+  color: var(--cru-clickable-normal-color);
+  font-size: 1.4rem;
+  background: none;
+  border: none;
+  transition: all 0.5s;
+  cursor: pointer;
+  user-select: none;
+}
+
+.cru-icon-button:hover {
+  color: var(--cru-clickable-hover-color);
+}
+
+.cru-icon-button:focus {
+  color: var(--cru-clickable-focus-color);
+}
+
+.cru-icon-button:active {
+  color: var(--cru-clickable-active-color);
+}
+
+.cru-flat-button:disabled {
+  color: var(--cru-clickable-disabled-color);
+  cursor: auto;
+}
+
+.cru-icon-button.large {
+  font-size: 1.6rem;
+}
diff --git a/FrontEnd/src/components/button/IconButton.tsx b/FrontEnd/src/components/button/IconButton.tsx
new file mode 100644
index 00000000..95c58887
--- /dev/null
+++ b/FrontEnd/src/components/button/IconButton.tsx
@@ -0,0 +1,30 @@
+import { ComponentPropsWithoutRef } from "react";
+import classNames from "classnames";
+
+import { ThemeColor } from "../common";
+
+import "./IconButton.css";
+
+interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
+  icon: string;
+  color?: ThemeColor | "grayscale";
+  large?: boolean;
+  disabled?: boolean; // TODO: Not implemented
+}
+
+export default function IconButton(props: IconButtonProps) {
+  const { icon, color, className, large, ...otherProps } = props;
+
+  return (
+    <button
+      className={classNames(
+        "cru-icon-button",
+        `cru-clickable-${color ?? "grayscale"}`,
+        large && "large",
+        "bi-" + icon,
+        className,
+      )}
+      {...otherProps}
+    />
+  );
+}
diff --git a/FrontEnd/src/components/button/LoadingButton.css b/FrontEnd/src/components/button/LoadingButton.css
new file mode 100644
index 00000000..23fadd3d
--- /dev/null
+++ b/FrontEnd/src/components/button/LoadingButton.css
@@ -0,0 +1,13 @@
+.cru-loading-button {
+  display: flex;
+  align-items: center;
+}
+
+.cru-loading-button-spinner {
+  margin-left: 0.5em;
+}
+
+.cru-loading-button-loading {
+  color: var(--cru-clickable-normal-color) !important;
+  border-color: var(--cru-clickable-normal-color) !important;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/button/LoadingButton.tsx b/FrontEnd/src/components/button/LoadingButton.tsx
new file mode 100644
index 00000000..7e7d08e6
--- /dev/null
+++ b/FrontEnd/src/components/button/LoadingButton.tsx
@@ -0,0 +1,40 @@
+import classNames from "classnames";
+
+import { I18nText, ThemeColor, useC } from "../common";
+
+import Spinner from "../Spinner";
+
+import "./LoadingButton.css";
+
+interface LoadingButtonProps extends React.ComponentPropsWithoutRef<"button"> {
+  color?: ThemeColor;
+  text?: I18nText;
+  loading?: boolean;
+}
+
+export default function LoadingButton(props: LoadingButtonProps) {
+  const c = useC();
+
+  const { color, text, loading, disabled, className, children, ...otherProps } =
+    props;
+
+  if (text != null && children != null) {
+    console.warn("You can't set both text and children props.");
+  }
+
+  return (
+    <button
+      disabled={disabled || loading}
+      className={classNames(
+        "cru-button outline cru-loading-button",
+        `cru-clickable-${color ?? "primary"}`,
+        loading && "cru-loading-button-loading",
+        className,
+      )}
+      {...otherProps}
+    >
+      {text != null ? c(text) : children}
+      {loading && <Spinner className="cru-loading-button-spinner" />}
+    </button>
+  );
+}
diff --git a/FrontEnd/src/components/button/index.tsx b/FrontEnd/src/components/button/index.tsx
new file mode 100644
index 00000000..b5aa5470
--- /dev/null
+++ b/FrontEnd/src/components/button/index.tsx
@@ -0,0 +1,15 @@
+import Button from "./Button";
+import FlatButton from "./FlatButton";
+import IconButton from "./IconButton";
+import LoadingButton from "./LoadingButton";
+import ButtonRow from "./ButtonRow";
+import ButtonRowV2 from "./ButtonRowV2";
+
+export {
+  Button,
+  FlatButton,
+  IconButton,
+  LoadingButton,
+  ButtonRow,
+  ButtonRowV2,
+};
diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts
new file mode 100644
index 00000000..e6f7319f
--- /dev/null
+++ b/FrontEnd/src/components/common.ts
@@ -0,0 +1,14 @@
+export type { Text, I18nText } from "~src/common";
+export { UiLogicError, c, convertI18nText, useC } from "~src/common";
+
+export const themeColors = [
+  "primary",
+  "secondary",
+  "danger",
+  "create",
+] as const;
+
+export type ThemeColor = (typeof themeColors)[number];
+
+export { breakpoints } from "./breakpoints";
+export { useMobile } from "./hooks";
diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.css b/FrontEnd/src/components/dialog/ConfirmDialog.css
new file mode 100644
index 00000000..e69de29b
diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx
new file mode 100644
index 00000000..26939c9b
--- /dev/null
+++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx
@@ -0,0 +1,59 @@
+import { useC, Text, ThemeColor } from "../common";
+
+import Dialog from "./Dialog";
+import DialogContainer from "./DialogContainer";
+
+export default function ConfirmDialog({
+  open,
+  onClose,
+  onConfirm,
+  title,
+  body,
+  color,
+  bodyColor,
+}: {
+  open: boolean;
+  onClose: () => void;
+  onConfirm: () => void;
+  title: Text;
+  body: Text;
+  color?: ThemeColor;
+  bodyColor?: ThemeColor;
+}) {
+  const c = useC();
+
+  return (
+    <Dialog onClose={onClose} open={open}>
+      <DialogContainer
+        title={title}
+        titleColor={color ?? "danger"}
+        buttons={[
+          {
+            key: "cancel",
+            type: "normal",
+            props: {
+              text: "operationDialog.cancel",
+              color: "secondary",
+              outline: true,
+              onClick: onClose,
+            },
+          },
+          {
+            key: "confirm",
+            type: "normal",
+            props: {
+              text: "operationDialog.confirm",
+              color: "danger",
+              onClick: () => {
+                onConfirm();
+                onClose();
+              },
+            },
+          },
+        ]}
+      >
+        <div className={`cru-${bodyColor ?? "primary"}`}>{c(body)}</div>
+      </DialogContainer>
+    </Dialog>
+  );
+}
diff --git a/FrontEnd/src/components/dialog/Dialog.css b/FrontEnd/src/components/dialog/Dialog.css
new file mode 100644
index 00000000..e4c61440
--- /dev/null
+++ b/FrontEnd/src/components/dialog/Dialog.css
@@ -0,0 +1,60 @@
+.cru-dialog-overlay {
+  position: fixed;
+  z-index: 1040;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  overflow: auto;
+}
+
+.cru-dialog-background {
+  position: absolute;
+  z-index: -1;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background-color: var(--cru-surface-dim-color);
+  opacity: 0.8;
+}
+
+.cru-dialog-container {
+  max-width: 100%;
+  min-width: 30vw;
+
+  margin: 2em auto;
+
+  border: var(--cru-key-container-color) 1px solid;
+  border-radius: 5px;
+  padding: 1.5em;
+  background-color: var(--cru-surface-color);
+}
+
+@media (min-width: 576px) {
+  .cru-dialog-container {
+    max-width: 800px;
+  }
+}
+
+.cru-dialog-enter .cru-dialog-container {
+  transform: scale(0, 0);
+  opacity: 0;
+  transform-origin: center;
+}
+
+.cru-dialog-enter-active .cru-dialog-container {
+  transform: scale(1, 1);
+  opacity: 1;
+  transition: transform 0.3s, opacity 0.3s;
+  transform-origin: center;
+}
+
+.cru-dialog-exit-active .cru-dialog-container {
+  transition: transform 0.3s, opacity 0.3s;
+  transform: scale(0, 0);
+  opacity: 0;
+  transform-origin: center;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/dialog/Dialog.tsx b/FrontEnd/src/components/dialog/Dialog.tsx
new file mode 100644
index 00000000..2ff7bea8
--- /dev/null
+++ b/FrontEnd/src/components/dialog/Dialog.tsx
@@ -0,0 +1,63 @@
+import { ReactNode, useRef } from "react";
+import ReactDOM from "react-dom";
+import { CSSTransition } from "react-transition-group";
+import classNames from "classnames";
+
+import { ThemeColor } from "../common";
+
+import "./Dialog.css";
+
+const optionalPortalElement = document.getElementById("portal");
+if (optionalPortalElement == null) {
+  throw new Error("Portal element not found");
+}
+const portalElement = optionalPortalElement;
+
+interface DialogProps {
+  open: boolean;
+  onClose: () => void;
+  color?: ThemeColor;
+  children?: ReactNode;
+  disableCloseOnClickOnOverlay?: boolean;
+}
+
+export default function Dialog({
+  open,
+  onClose,
+  color,
+  children,
+  disableCloseOnClickOnOverlay,
+}: DialogProps) {
+  color = color ?? "primary";
+
+  const nodeRef = useRef(null);
+
+  return ReactDOM.createPortal(
+    <CSSTransition
+      nodeRef={nodeRef}
+      mountOnEnter
+      unmountOnExit
+      in={open}
+      timeout={300}
+      classNames="cru-dialog"
+    >
+      <div
+        ref={nodeRef}
+        className={classNames("cru-dialog-overlay", `cru-${color}`)}
+      >
+        <div
+          className="cru-dialog-background"
+          onClick={
+            disableCloseOnClickOnOverlay
+              ? undefined
+              : () => {
+                  onClose();
+                }
+          }
+        />
+        <div className="cru-dialog-container">{children}</div>
+      </div>
+    </CSSTransition>,
+    portalElement,
+  );
+}
diff --git a/FrontEnd/src/components/dialog/DialogContainer.css b/FrontEnd/src/components/dialog/DialogContainer.css
new file mode 100644
index 00000000..fbb18e0d
--- /dev/null
+++ b/FrontEnd/src/components/dialog/DialogContainer.css
@@ -0,0 +1,20 @@
+.cru-dialog-container-title {
+  font-size: 1.2em;
+  font-weight: bold;
+  color: var(--cru-key-color);
+  margin-bottom: 0.5em;
+}
+
+
+.cru-dialog-container-hr {
+  margin: 1em 0;
+}
+
+.cru-dialog-container-button-row {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.cru-dialog-container-button {
+  margin-left: 1em;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/dialog/DialogContainer.tsx b/FrontEnd/src/components/dialog/DialogContainer.tsx
new file mode 100644
index 00000000..afee2669
--- /dev/null
+++ b/FrontEnd/src/components/dialog/DialogContainer.tsx
@@ -0,0 +1,95 @@
+import { ComponentProps, Ref, ReactNode } from "react";
+import classNames from "classnames";
+
+import { ThemeColor, Text, useC } from "../common";
+import { ButtonRow, ButtonRowV2 } from "../button";
+
+import "./DialogContainer.css";
+
+interface DialogContainerBaseProps {
+  className?: string;
+  title: Text;
+  titleColor?: ThemeColor;
+  titleClassName?: string;
+  titleRef?: Ref<HTMLDivElement>;
+  bodyContainerClassName?: string;
+  bodyContainerRef?: Ref<HTMLDivElement>;
+  buttonsClassName?: string;
+  buttonsContainerRef?: ComponentProps<typeof ButtonRow>["containerRef"];
+  children: ReactNode;
+}
+
+interface DialogContainerWithButtonsProps extends DialogContainerBaseProps {
+  buttons: ComponentProps<typeof ButtonRow>["buttons"];
+}
+
+interface DialogContainerWithButtonsV2Props extends DialogContainerBaseProps {
+  buttonsV2: ComponentProps<typeof ButtonRowV2>["buttons"];
+}
+
+type DialogContainerProps =
+  | DialogContainerWithButtonsProps
+  | DialogContainerWithButtonsV2Props;
+
+export default function DialogContainer(props: DialogContainerProps) {
+  const {
+    className,
+    title,
+    titleColor,
+    titleClassName,
+    titleRef,
+    bodyContainerClassName,
+    bodyContainerRef,
+    buttonsClassName,
+    buttonsContainerRef,
+    children,
+  } = props;
+
+  const c = useC();
+
+  return (
+    <div className={classNames(className)}>
+      <div
+        ref={titleRef}
+        className={classNames(
+          `cru-dialog-container-title cru-${titleColor ?? "primary"}`,
+          titleClassName,
+        )}
+      >
+        {c(title)}
+      </div>
+      <hr className="cru-dialog-container-hr" />
+      <div
+        ref={bodyContainerRef}
+        className={classNames(
+          "cru-dialog-container-body",
+          bodyContainerClassName,
+        )}
+      >
+        {children}
+      </div>
+      <hr className="cru-dialog-container-hr" />
+      {"buttons" in props ? (
+        <ButtonRow
+          containerRef={buttonsContainerRef}
+          className={classNames(
+            "cru-dialog-container-button-row",
+            buttonsClassName,
+          )}
+          buttons={props.buttons}
+          buttonsClassName="cru-dialog-container-button"
+        />
+      ) : (
+        <ButtonRowV2
+          containerRef={buttonsContainerRef}
+          className={classNames(
+            "cru-dialog-container-button-row",
+            buttonsClassName,
+          )}
+          buttons={props.buttonsV2}
+          buttonsClassName="cru-dialog-container-button"
+        />
+      )}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/dialog/FullPageDialog.css b/FrontEnd/src/components/dialog/FullPageDialog.css
new file mode 100644
index 00000000..2f1fc636
--- /dev/null
+++ b/FrontEnd/src/components/dialog/FullPageDialog.css
@@ -0,0 +1,44 @@
+.cru-full-page {
+  position: fixed;
+  z-index: 1030;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background-color: white;
+  padding-top: 56px;
+}
+
+.cru-full-page-top-bar {
+  height: 56px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1;
+  background-color: var(--cru-primary-color);
+  display: flex;
+  align-items: center;
+}
+
+.cru-full-page-content-container {
+  overflow: scroll;
+}
+
+.cru-full-page-back-button {
+  color: var(--cru-primary-t-color);
+}
+
+.cru-full-page-enter {
+  transform: translate(100%, 0);
+}
+
+.cru-full-page-enter-active {
+  transform: none;
+  transition: transform 0.3s;
+}
+
+.cru-full-page-exit-active {
+  transition: transform 0.3s;
+  transform: translate(100%, 0);
+}
diff --git a/FrontEnd/src/components/dialog/FullPageDialog.tsx b/FrontEnd/src/components/dialog/FullPageDialog.tsx
new file mode 100644
index 00000000..6368fc0a
--- /dev/null
+++ b/FrontEnd/src/components/dialog/FullPageDialog.tsx
@@ -0,0 +1,53 @@
+import * as React from "react";
+import { createPortal } from "react-dom";
+import classnames from "classnames";
+import { CSSTransition } from "react-transition-group";
+
+import "./FullPageDialog.css";
+import IconButton from "../button/IconButton";
+
+export interface FullPageDialogProps {
+  show: boolean;
+  onBack: () => void;
+  contentContainerClassName?: string;
+  children: React.ReactNode;
+}
+
+const FullPageDialog: React.FC<FullPageDialogProps> = ({
+  show,
+  onBack,
+  children,
+  contentContainerClassName,
+}) => {
+  return createPortal(
+    <CSSTransition
+      mountOnEnter
+      unmountOnExit
+      in={show}
+      timeout={300}
+      classNames="cru-full-page"
+    >
+      <div className="cru-full-page">
+        <div className="cru-full-page-top-bar">
+          <IconButton
+            icon="arrow-left"
+            className="ms-3 cru-full-page-back-button"
+            onClick={onBack}
+          />
+        </div>
+        <div
+          className={classnames(
+            "cru-full-page-content-container",
+            contentContainerClassName
+          )}
+        >
+          {children}
+        </div>
+      </div>
+    </CSSTransition>,
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    document.getElementById("portal")!
+  );
+};
+
+export default FullPageDialog;
diff --git a/FrontEnd/src/components/dialog/OperationDialog.css b/FrontEnd/src/components/dialog/OperationDialog.css
new file mode 100644
index 00000000..f4b7237e
--- /dev/null
+++ b/FrontEnd/src/components/dialog/OperationDialog.css
@@ -0,0 +1,8 @@
+.cru-operation-dialog-prompt {
+  color: var(--cru-surface-on-color);
+}
+
+.cru-operation-dialog-input-group {
+  display: block;
+  margin: 0.5em 0;
+}
diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx
new file mode 100644
index 00000000..e5db7f4f
--- /dev/null
+++ b/FrontEnd/src/components/dialog/OperationDialog.tsx
@@ -0,0 +1,230 @@
+import { useState, ReactNode, ComponentProps } from "react";
+import classNames from "classnames";
+
+import { useC, Text, ThemeColor } from "../common";
+
+import {
+  useInputs,
+  InputGroup,
+  Initializer as InputInitializer,
+  InputValueDict,
+  InputErrorDict,
+  InputConfirmValueDict,
+} from "../input";
+import Dialog from "./Dialog";
+import DialogContainer from "./DialogContainer";
+import { ButtonRow } from "../button";
+
+import "./OperationDialog.css";
+
+export type { InputInitializer, InputValueDict, InputErrorDict };
+
+interface OperationDialogPromptProps {
+  message?: Text;
+  customMessage?: Text;
+  customMessageNode?: ReactNode;
+  className?: string;
+}
+
+function OperationDialogPrompt(props: OperationDialogPromptProps) {
+  const { message, customMessage, customMessageNode, className } = props;
+
+  const c = useC();
+
+  return (
+    <div className={classNames(className, "cru-operation-dialog-prompt")}>
+      {message && <p>{c(message)}</p>}
+      {customMessageNode ?? (customMessage != null ? c(customMessage) : null)}
+    </div>
+  );
+}
+
+export interface OperationDialogProps<TData> {
+  open: boolean;
+  onClose: () => void;
+
+  color?: ThemeColor;
+  inputColor?: ThemeColor;
+  title: Text;
+  inputPrompt?: Text;
+  inputPromptNode?: ReactNode;
+  successPrompt?: (data: TData) => Text;
+  successPromptNode?: (data: TData) => ReactNode;
+  failurePrompt?: (error: unknown) => Text;
+  failurePromptNode?: (error: unknown) => ReactNode;
+
+  inputs: InputInitializer;
+
+  onProcess: (inputs: InputConfirmValueDict) => Promise<TData>;
+  onSuccessAndClose?: (data: TData) => void;
+}
+
+function OperationDialog<TData>(props: OperationDialogProps<TData>) {
+  const {
+    open,
+    onClose,
+    color,
+    inputColor,
+    title,
+    inputPrompt,
+    inputPromptNode,
+    successPrompt,
+    successPromptNode,
+    failurePrompt,
+    failurePromptNode,
+    inputs,
+    onProcess,
+    onSuccessAndClose,
+  } = props;
+
+  if (process.env.NODE_ENV === "development") {
+    if (inputPrompt && inputPromptNode) {
+      console.log("InputPrompt and inputPromptNode are both set.");
+    }
+    if (successPrompt && successPromptNode) {
+      console.log("SuccessPrompt and successPromptNode are both set.");
+    }
+    if (failurePrompt && failurePromptNode) {
+      console.log("FailurePrompt and failurePromptNode are both set.");
+    }
+  }
+
+  type Step =
+    | { type: "input" }
+    | { type: "process" }
+    | {
+        type: "success";
+        data: TData;
+      }
+    | {
+        type: "failure";
+        data: unknown;
+      };
+
+  const [step, setStep] = useState<Step>({ type: "input" });
+
+  const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } =
+    useInputs({
+      init: inputs,
+    });
+
+  function close() {
+    if (step.type !== "process") {
+      onClose();
+      if (step.type === "success" && onSuccessAndClose) {
+        onSuccessAndClose?.(step.data);
+      }
+    } else {
+      console.log("Attempt to close modal dialog when processing.");
+    }
+  }
+
+  function onConfirm() {
+    const result = confirm();
+    if (result.type === "ok") {
+      setStep({ type: "process" });
+      setAllDisabled(true);
+      onProcess(result.values).then(
+        (d) => {
+          setStep({
+            type: "success",
+            data: d,
+          });
+        },
+        (e: unknown) => {
+          setStep({
+            type: "failure",
+            data: e,
+          });
+        },
+      );
+    }
+  }
+
+  let body: ReactNode;
+  let buttons: ComponentProps<typeof ButtonRow>["buttons"];
+
+  if (step.type === "input" || step.type === "process") {
+    const isProcessing = step.type === "process";
+
+    body = (
+      <div>
+        <OperationDialogPrompt
+          customMessage={inputPrompt}
+          customMessageNode={inputPromptNode}
+        />
+        <InputGroup
+          containerClassName="cru-operation-dialog-input-group"
+          color={inputColor ?? "primary"}
+          {...inputGroupProps}
+        />
+      </div>
+    );
+    buttons = [
+      {
+        key: "cancel",
+        type: "normal",
+        props: {
+          text: "operationDialog.cancel",
+          color: "secondary",
+          outline: true,
+          onClick: close,
+          disabled: isProcessing,
+        },
+      },
+      {
+        key: "confirm",
+        type: "loading",
+        props: {
+          text: "operationDialog.confirm",
+          color,
+          loading: isProcessing,
+          disabled: hasErrorAndDirty,
+          onClick: onConfirm,
+        },
+      },
+    ];
+  } else {
+    const result = step;
+
+    const promptProps: OperationDialogPromptProps =
+      result.type === "success"
+        ? {
+            message: "operationDialog.success",
+            customMessage: successPrompt?.(result.data),
+            customMessageNode: successPromptNode?.(result.data),
+          }
+        : {
+            message: "operationDialog.error",
+            customMessage: failurePrompt?.(result.data),
+            customMessageNode: failurePromptNode?.(result.data),
+          };
+    body = (
+      <div>
+        <OperationDialogPrompt {...promptProps} />
+      </div>
+    );
+
+    buttons = [
+      {
+        key: "ok",
+        type: "normal",
+        props: {
+          text: "operationDialog.ok",
+          color: "primary",
+          onClick: close,
+        },
+      },
+    ];
+  }
+
+  return (
+    <Dialog open={open} onClose={close}>
+      <DialogContainer title={title} titleColor={color} buttons={buttons}>
+        {body}
+      </DialogContainer>
+    </Dialog>
+  );
+}
+
+export default OperationDialog;
diff --git a/FrontEnd/src/components/dialog/index.ts b/FrontEnd/src/components/dialog/index.ts
new file mode 100644
index 00000000..59f15791
--- /dev/null
+++ b/FrontEnd/src/components/dialog/index.ts
@@ -0,0 +1,64 @@
+import { useState } from "react";
+
+export { default as Dialog } from "./Dialog";
+export { default as FullPageDialog } from "./FullPageDialog";
+export { default as OperationDialog } from "./OperationDialog";
+export { default as ConfirmDialog } from "./ConfirmDialog";
+
+type DialogMap<D extends string, V> = {
+  [K in D]: V;
+};
+
+type DialogKeyMap<D extends string> = DialogMap<D, number>;
+
+type DialogPropsMap<D extends string> = DialogMap<
+  D,
+  { key: number | string; open: boolean; onClose: () => void }
+>;
+
+export function useDialog<D extends string>(
+  dialogs: D[],
+  options?: {
+    initDialog?: D | null;
+    onClose?: {
+      [K in D]?: () => void;
+    };
+  },
+): {
+  dialog: D | null;
+  switchDialog: (newDialog: D | null) => void;
+  dialogPropsMap: DialogPropsMap<D>;
+  createDialogSwitch: (newDialog: D | null) => () => void;
+} {
+  const [dialog, setDialog] = useState<D | null>(options?.initDialog ?? null);
+
+  const [dialogKeys, setDialogKeys] = useState<DialogKeyMap<D>>(
+    () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap<D>,
+  );
+
+  const switchDialog = (newDialog: D | null) => {
+    if (dialog !== null) {
+      setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 });
+    }
+    setDialog(newDialog);
+  };
+
+  return {
+    dialog,
+    switchDialog,
+    dialogPropsMap: Object.fromEntries(
+      dialogs.map((d) => [
+        d,
+        {
+          key: `${d}-${dialogKeys[d]}`,
+          open: dialog === d,
+          onClose: () => {
+            switchDialog(null);
+            options?.onClose?.[d]?.();
+          },
+        },
+      ]),
+    ) as DialogPropsMap<D>,
+    createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog),
+  };
+}
diff --git a/FrontEnd/src/components/hooks.ts b/FrontEnd/src/components/hooks.ts
new file mode 100644
index 00000000..523a4538
--- /dev/null
+++ b/FrontEnd/src/components/hooks.ts
@@ -0,0 +1,14 @@
+// TODO: Migrate hooks
+
+export {
+  useIsSmallScreen,
+  useClickOutside,
+  useScrollToBottom,
+} from "~src/utilities/hooks";
+
+import { useMediaQuery } from "react-responsive";
+import { breakpoints } from "./breakpoints";
+
+export function useMobile(): boolean {
+  return useMediaQuery({ maxWidth: breakpoints.sm });
+}
diff --git a/FrontEnd/src/components/index.css b/FrontEnd/src/components/index.css
new file mode 100644
index 00000000..a8f5e9a5
--- /dev/null
+++ b/FrontEnd/src/components/index.css
@@ -0,0 +1,100 @@
+@import "./theme.css";
+
+* {
+  box-sizing: border-box;
+  margin-inline: 0;
+  margin-block: 0;
+}
+
+body {
+  font-family: var(--cru-default-font-family);
+  background: var(--cru-body-background-color);
+  color: var(--cru-text-primary-color);
+  line-height: 1.2;
+}
+
+.cru-page {
+  padding: var(--cru-page-padding);
+}
+
+.cru-page-no-top-padding {
+  padding-top: 0;
+}
+
+.cru-text-center {
+  text-align: center;
+}
+
+.cru-text-end {
+  text-align: end;
+}
+
+.cru-float-left {
+  float: left;
+}
+
+.cru-float-right {
+  float: right;
+}
+
+.cru-align-text-bottom {
+  vertical-align: text-bottom;
+}
+
+.cru-align-middle {
+  vertical-align: middle;
+}
+
+.cru-clearfix::after {
+  clear: both;
+}
+
+.cru-fill-parent {
+  width: 100%;
+  height: 100%;
+}
+
+.cru-avatar {
+  width: 60px;
+  height: 60px;
+}
+
+.cru-avatar.large {
+  width: 100px;
+  height: 100px;
+}
+
+.cru-avatar.small {
+  width: 40px;
+  height: 40px;
+}
+
+.cru-round {
+  border-radius: 50%;
+}
+
+.cru-tab-pages-action-area {
+  display: flex;
+  align-items: center;
+}
+
+.alert-container {
+  position: fixed;
+  z-index: 1070;
+}
+
+@media (min-width: 576px) {
+  .alert-container {
+    bottom: 0;
+    right: 0;
+  }
+}
+
+@media (max-width: 575.98px) {
+  .alert-container {
+    bottom: 0;
+    right: 0;
+    left: 0;
+    text-align: center;
+  }
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/input/InputGroup.css b/FrontEnd/src/components/input/InputGroup.css
new file mode 100644
index 00000000..7e905b1e
--- /dev/null
+++ b/FrontEnd/src/components/input/InputGroup.css
@@ -0,0 +1,54 @@
+.cru-input-group {
+  display: block;
+}
+
+.cru-input-container {
+  margin: 0.4em 0;
+}
+
+.cru-input-label {
+  display: block;
+  color: var(--cru-clickable-normal-color);
+  font-size: 0.9em;
+  margin-bottom: 0.3em;
+}
+
+.cru-input-label-inline {
+  margin-inline-start: 0.5em;
+}
+
+.cru-input-type-text input {
+  appearance: none;
+  display: block;
+  border: 1px solid;
+  /* color: var(--cru-surface-on-color); */
+  /* background-color: var(--cru-surface-color); */
+  margin: 0;
+  font-size: 1em;
+  padding: 0.2em;
+}
+
+.cru-input-type-text input:hover {
+  border-color: var(--cru-clickable-hover-color);
+}
+
+.cru-input-type-text input:focus {
+  border-color: var(--cru-clickable-focus-color);
+}
+
+.cru-input-type-text input:disabled {
+  border-color: var(--cru-clickable-disabled-color);
+}
+
+.cru-input-error {
+  display: block;
+  font-size: 0.8em;
+  color: var(--cru-danger-color);
+  margin-top: 0.4em;
+}
+
+.cru-input-helper {
+  display: block;
+  font-size: 0.8em;
+  color: var(--cru-primary-color);
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/input/InputGroup.tsx b/FrontEnd/src/components/input/InputGroup.tsx
new file mode 100644
index 00000000..4f487344
--- /dev/null
+++ b/FrontEnd/src/components/input/InputGroup.tsx
@@ -0,0 +1,463 @@
+/**
+ * Some notes for InputGroup:
+ * This is one of the most complicated components in this project.
+ * Probably because the feature is complex and involved user inputs.
+ *
+ * I hope it contains following features:
+ * - Input features
+ *   - Supports a wide range of input types.
+ *   - Validator to validate user inputs.
+ *   - Can set initial values.
+ *   - Dirty, aka, has user touched this input.
+ * - Developer friendly
+ *   - Easy to use APIs.
+ *   - Type check as much as possible.
+ * - UI
+ *   - Configurable appearance.
+ *   - Can display helper and error messages.
+ * - Easy to extend, like new input types.
+ *
+ * So here is some design decisions:
+ * Inputs are identified by its _key_.
+ * `InputGroup` component takes care of only UI and no logic.
+ * `useInputs` hook takes care of logic and generate props for `InputGroup`.
+ */
+
+import { useState, Ref, useId } from "react";
+import classNames from "classnames";
+
+import { useC, Text, ThemeColor } from "../common";
+
+import "./InputGroup.css";
+
+export interface InputBase {
+  key: string;
+  label: Text;
+  helper?: Text;
+  disabled?: boolean;
+  error?: Text;
+}
+
+export interface TextInput extends InputBase {
+  type: "text";
+  value: string;
+  password?: boolean;
+}
+
+export interface BoolInput extends InputBase {
+  type: "bool";
+  value: boolean;
+}
+
+export interface SelectInputOption {
+  value: string;
+  label: Text;
+  icon?: string;
+}
+
+export interface SelectInput extends InputBase {
+  type: "select";
+  value: string;
+  options: SelectInputOption[];
+}
+
+export type Input = TextInput | BoolInput | SelectInput;
+
+export type InputValue = Input["value"];
+
+export type InputValueDict = Record<string, InputValue>;
+export type InputErrorDict = Record<string, Text>;
+export type InputDisabledDict = Record<string, boolean>;
+export type InputDirtyDict = Record<string, boolean>;
+// use never so you don't have to cast everywhere
+export type InputConfirmValueDict = Record<string, never>;
+
+export type GeneralInputErrorDict =
+  | {
+      [key: string]: Text | null | undefined;
+    }
+  | null
+  | undefined;
+
+type MakeInputInfo<I extends Input> = Omit<I, "value" | "error" | "disabled">;
+
+export type InputInfo = {
+  [I in Input as I["type"]]: MakeInputInfo<I>;
+}[Input["type"]];
+
+export type Validator = (
+  values: InputValueDict,
+  inputs: InputInfo[],
+) => GeneralInputErrorDict;
+
+export type InputScheme = {
+  inputs: InputInfo[];
+  validator?: Validator;
+};
+
+export type InputData = {
+  values: InputValueDict;
+  errors: InputErrorDict;
+  disabled: InputDisabledDict;
+  dirties: InputDirtyDict;
+};
+
+export type State = {
+  scheme: InputScheme;
+  data: InputData;
+};
+
+export type DataInitialization = {
+  values?: InputValueDict;
+  errors?: GeneralInputErrorDict;
+  disabled?: InputDisabledDict;
+  dirties?: InputDirtyDict;
+};
+
+export type Initialization = {
+  scheme: InputScheme;
+  dataInit?: DataInitialization;
+};
+
+export type GeneralInitialization = Initialization | InputScheme | InputInfo[];
+
+export type Initializer = GeneralInitialization | (() => GeneralInitialization);
+
+export interface InputGroupProps {
+  color?: ThemeColor;
+  containerClassName?: string;
+  containerRef?: Ref<HTMLDivElement>;
+
+  inputs: Input[];
+  onChange: (index: number, value: Input["value"]) => void;
+}
+
+function cleanObject<V>(o: Record<string, V>): Record<string, NonNullable<V>> {
+  const result = { ...o };
+  for (const key of Object.keys(result)) {
+    if (result[key] == null) {
+      delete result[key];
+    }
+  }
+  return result as never;
+}
+
+export type ConfirmResult =
+  | {
+      type: "ok";
+      values: InputConfirmValueDict;
+    }
+  | {
+      type: "error";
+      errors: InputErrorDict;
+    };
+
+function validate(
+  validator: Validator | null | undefined,
+  values: InputValueDict,
+  inputs: InputInfo[],
+): InputErrorDict {
+  return cleanObject(validator?.(values, inputs) ?? {});
+}
+
+export function useInputs(options: { init: Initializer }): {
+  inputGroupProps: InputGroupProps;
+  hasError: boolean;
+  hasErrorAndDirty: boolean;
+  confirm: () => ConfirmResult;
+  setAllDisabled: (disabled: boolean) => void;
+} {
+  function initializeValue(
+    input: InputInfo,
+    value?: InputValue | null,
+  ): InputValue {
+    if (input.type === "text") {
+      return value ?? "";
+    } else if (input.type === "bool") {
+      return value ?? false;
+    } else if (input.type === "select") {
+      return value ?? input.options[0].value;
+    }
+    throw new Error("Unknown input type");
+  }
+
+  function initialize(generalInitialization: GeneralInitialization): State {
+    const initialization: Initialization = Array.isArray(generalInitialization)
+      ? { scheme: { inputs: generalInitialization } }
+      : "scheme" in generalInitialization
+      ? generalInitialization
+      : { scheme: generalInitialization };
+
+    const { scheme, dataInit } = initialization;
+    const { inputs, validator } = scheme;
+    const keys = inputs.map((input) => input.key);
+
+    if (process.env.NODE_ENV === "development") {
+      const checkKeys = (dict: Record<string, unknown> | undefined) => {
+        if (dict != null) {
+          for (const key of Object.keys(dict)) {
+            if (!keys.includes(key)) {
+              console.warn("");
+            }
+          }
+        }
+      };
+
+      checkKeys(dataInit?.values);
+      checkKeys(dataInit?.errors ?? {});
+      checkKeys(dataInit?.disabled);
+      checkKeys(dataInit?.dirties);
+    }
+
+    function clean<V>(
+      dict: Record<string, V> | null | undefined,
+    ): Record<string, NonNullable<V>> {
+      return dict != null ? cleanObject(dict) : {};
+    }
+
+    const values: InputValueDict = {};
+    const disabled: InputDisabledDict = clean(dataInit?.disabled);
+    const dirties: InputDirtyDict = clean(dataInit?.dirties);
+    const isErrorSet = dataInit?.errors != null;
+    let errors: InputErrorDict = clean(dataInit?.errors);
+
+    for (let i = 0; i < inputs.length; i++) {
+      const input = inputs[i];
+      const { key } = input;
+
+      values[key] = initializeValue(input, dataInit?.values?.[key]);
+    }
+
+    if (isErrorSet) {
+      if (process.env.NODE_ENV === "development") {
+        console.log(
+          "You explicitly set errors (not undefined) in initializer, so validator won't run.",
+        );
+      }
+    } else {
+      errors = validate(validator, values, inputs);
+    }
+
+    return {
+      scheme,
+      data: {
+        values,
+        errors,
+        disabled,
+        dirties,
+      },
+    };
+  }
+
+  const { init } = options;
+  const initializer = typeof init === "function" ? init : () => init;
+
+  const [state, setState] = useState<State>(() => initialize(initializer()));
+
+  const { scheme, data } = state;
+  const { validator } = scheme;
+
+  function createAllBooleanDict(value: boolean): Record<string, boolean> {
+    const result: InputDirtyDict = {};
+    for (const key of scheme.inputs.map((input) => input.key)) {
+      result[key] = value;
+    }
+    return result;
+  }
+
+  const createAllDirties = () => createAllBooleanDict(true);
+
+  const componentInputs: Input[] = [];
+
+  for (let i = 0; i < scheme.inputs.length; i++) {
+    const input = scheme.inputs[i];
+    const value = data.values[input.key];
+    const error = data.errors[input.key];
+    const disabled = data.disabled[input.key] ?? false;
+    const dirty = data.dirties[input.key] ?? false;
+    const componentInput: Input = {
+      ...input,
+      value: value as never,
+      disabled,
+      error: dirty ? error : undefined,
+    };
+    componentInputs.push(componentInput);
+  }
+
+  const hasError = Object.keys(data.errors).length > 0;
+  const hasDirty = Object.keys(data.dirties).some((key) => data.dirties[key]);
+
+  return {
+    inputGroupProps: {
+      inputs: componentInputs,
+      onChange: (index, value) => {
+        const input = scheme.inputs[index];
+        const { key } = input;
+        const newValues = { ...data.values, [key]: value };
+        const newDirties = { ...data.dirties, [key]: true };
+        const newErrors = validate(validator, newValues, scheme.inputs);
+        setState({
+          scheme,
+          data: {
+            ...data,
+            values: newValues,
+            errors: newErrors,
+            dirties: newDirties,
+          },
+        });
+      },
+    },
+    hasError,
+    hasErrorAndDirty: hasError && hasDirty,
+    confirm() {
+      const newDirties = createAllDirties();
+      const newErrors = validate(validator, data.values, scheme.inputs);
+
+      setState({
+        scheme,
+        data: {
+          ...data,
+          dirties: newDirties,
+          errors: newErrors,
+        },
+      });
+
+      if (Object.keys(newErrors).length !== 0) {
+        return {
+          type: "error",
+          errors: newErrors,
+        };
+      } else {
+        return {
+          type: "ok",
+          values: data.values as InputConfirmValueDict,
+        };
+      }
+    },
+    setAllDisabled(disabled: boolean) {
+      setState({
+        scheme,
+        data: {
+          ...data,
+          disabled: createAllBooleanDict(disabled),
+        },
+      });
+    },
+  };
+}
+
+export function InputGroup({
+  color,
+  inputs,
+  onChange,
+  containerRef,
+  containerClassName,
+}: InputGroupProps) {
+  const c = useC();
+
+  const id = useId();
+
+  return (
+    <div
+      ref={containerRef}
+      className={classNames(
+        "cru-input-group",
+        `cru-clickable-${color ?? "primary"}`,
+        containerClassName,
+      )}
+    >
+      {inputs.map((item, index) => {
+        const { key, type, value, label, error, helper, disabled } = item;
+
+        const getContainerClassName = (
+          ...additionalClassNames: classNames.ArgumentArray
+        ) =>
+          classNames(
+            `cru-input-container cru-input-type-${type}`,
+            error && "error",
+            ...additionalClassNames,
+          );
+
+        const changeValue = (value: InputValue) => {
+          onChange(index, value);
+        };
+
+        const inputId = `${id}-${key}`;
+
+        if (type === "text") {
+          const { password } = item;
+          return (
+            <div
+              key={key}
+              className={getContainerClassName(password && "password")}
+            >
+              {label && (
+                <label className="cru-input-label" htmlFor={inputId}>
+                  {c(label)}
+                </label>
+              )}
+              <input
+                id={inputId}
+                type={password ? "password" : "text"}
+                value={value}
+                onChange={(event) => {
+                  const v = event.target.value;
+                  changeValue(v);
+                }}
+                disabled={disabled}
+              />
+              {error && <div className="cru-input-error">{c(error)}</div>}
+              {helper && <div className="cru-input-helper">{c(helper)}</div>}
+            </div>
+          );
+        } else if (type === "bool") {
+          return (
+            <div key={key} className={getContainerClassName()}>
+              <input
+                id={inputId}
+                type="checkbox"
+                checked={value}
+                onChange={(event) => {
+                  const v = event.currentTarget.checked;
+                  changeValue(v);
+                }}
+                disabled={disabled}
+              />
+              <label className="cru-input-label-inline" htmlFor={inputId}>
+                {c(label)}
+              </label>
+              {error && <div className="cru-input-error">{c(error)}</div>}
+              {helper && <div className="cru-input-helper">{c(helper)}</div>}
+            </div>
+          );
+        } else if (type === "select") {
+          return (
+            <div key={key} className={getContainerClassName()}>
+              <label className="cru-input-label" htmlFor={inputId}>
+                {c(label)}
+              </label>
+              <select
+                id={inputId}
+                value={value}
+                onChange={(event) => {
+                  const e = event.target.value;
+                  changeValue(e);
+                }}
+                disabled={disabled}
+              >
+                {item.options.map((option) => {
+                  return (
+                    <option value={option.value} key={option.value}>
+                      {option.icon}
+                      {c(option.label)}
+                    </option>
+                  );
+                })}
+              </select>
+            </div>
+          );
+        }
+      })}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/input/index.ts b/FrontEnd/src/components/input/index.ts
new file mode 100644
index 00000000..ca183089
--- /dev/null
+++ b/FrontEnd/src/components/input/index.ts
@@ -0,0 +1,11 @@
+export { useInputs, InputGroup } from "./InputGroup";
+
+export type {
+  InputValueDict,
+  InputErrorDict,
+  InputDirtyDict,
+  InputDisabledDict,
+  InputConfirmValueDict,
+  Validator,
+  Initializer,
+} from "./InputGroup";
diff --git a/FrontEnd/src/components/list/ListContainer.css b/FrontEnd/src/components/list/ListContainer.css
new file mode 100644
index 00000000..53781834
--- /dev/null
+++ b/FrontEnd/src/components/list/ListContainer.css
@@ -0,0 +1,4 @@
+.cru-list-container {
+  border: 1px solid var(--cru-clickable-primary-normal-color);
+  border-radius: 5px;
+}
diff --git a/FrontEnd/src/components/list/ListContainer.tsx b/FrontEnd/src/components/list/ListContainer.tsx
new file mode 100644
index 00000000..aa00d12c
--- /dev/null
+++ b/FrontEnd/src/components/list/ListContainer.tsx
@@ -0,0 +1,23 @@
+import { ComponentPropsWithoutRef, forwardRef, Ref } from "react";
+import classNames from "classnames";
+
+import "./ListContainer.css"
+
+function _ListContainer(
+  { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">,
+  ref: Ref<HTMLDivElement>,
+) {
+  return (
+    <div
+      ref={ref}
+      className={classNames("cru-list-container", className)}
+      {...otherProps}
+    >
+      {children}
+    </div>
+  );
+}
+
+const ListContainer = forwardRef(_ListContainer);
+
+export default ListContainer;
diff --git a/FrontEnd/src/components/list/ListItemContainer.css b/FrontEnd/src/components/list/ListItemContainer.css
new file mode 100644
index 00000000..8d7afa9f
--- /dev/null
+++ b/FrontEnd/src/components/list/ListItemContainer.css
@@ -0,0 +1,3 @@
+.cru-list-item-container {
+  border: 1px solid var(--cru-clickable-primary-normal-color);
+}
diff --git a/FrontEnd/src/components/list/ListItemContainer.tsx b/FrontEnd/src/components/list/ListItemContainer.tsx
new file mode 100644
index 00000000..315cbd6e
--- /dev/null
+++ b/FrontEnd/src/components/list/ListItemContainer.tsx
@@ -0,0 +1,23 @@
+import { ComponentPropsWithoutRef, forwardRef, Ref } from "react";
+import classNames from "classnames";
+
+import "./ListItemContainer.css";
+
+function _ListItemContainer(
+  { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">,
+  ref: Ref<HTMLDivElement>,
+) {
+  return (
+    <div
+      ref={ref}
+      className={classNames("cru-list-item-container", className)}
+      {...otherProps}
+    >
+      {children}
+    </div>
+  );
+}
+
+const ListItemContainer = forwardRef(_ListItemContainer);
+
+export default ListItemContainer;
diff --git a/FrontEnd/src/components/list/index.ts b/FrontEnd/src/components/list/index.ts
new file mode 100644
index 00000000..e183f7da
--- /dev/null
+++ b/FrontEnd/src/components/list/index.ts
@@ -0,0 +1,4 @@
+import ListContainer from "./ListContainer";
+import ListItemContainer from "./ListItemContainer";
+
+export { ListContainer, ListItemContainer };
diff --git a/FrontEnd/src/components/menu/Menu.css b/FrontEnd/src/components/menu/Menu.css
new file mode 100644
index 00000000..75734533
--- /dev/null
+++ b/FrontEnd/src/components/menu/Menu.css
@@ -0,0 +1,36 @@
+.cru-menu {
+  min-width: 200px;
+}
+
+.cru-menu-item {
+  display: block;
+  font-size: 1em;
+  width: 100%;
+  padding: 0.5em 1.5em;
+  transition: all 0.5s;
+  color: var(--cru-clickable-normal-color);
+  background-color: var(--cru-clickable-grayscale-normal-color);
+  border: none;
+  cursor: pointer;
+}
+
+.cru-menu-item:hover {
+  background-color: var(--cru-clickable-grayscale-hover-color);
+}
+
+.cru-menu-item:focus {
+  background-color: var(--cru-clickable-grayscale-focus-color);
+}
+
+.cru-menu-item:active {
+  background-color: var(--cru-clickable-grayscale-active-color);
+}
+
+.cru-menu-item-icon {
+  margin-right: 1em;
+}
+
+.cru-menu-divider {
+  border-width: 0;
+  border-top: 1px solid var(--cru-primary-color);
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx
new file mode 100644
index 00000000..e8099c76
--- /dev/null
+++ b/FrontEnd/src/components/menu/Menu.tsx
@@ -0,0 +1,62 @@
+import { CSSProperties } from "react";
+import classNames from "classnames";
+
+import { useC, Text, ThemeColor } from "../common";
+
+import "./Menu.css";
+import Icon from "../Icon";
+
+export type MenuItem =
+  | {
+      type: "divider";
+    }
+  | {
+      type: "button";
+      text: Text;
+      icon?: string;
+      color?: ThemeColor;
+      onClick: () => void;
+    };
+
+export type MenuItems = MenuItem[];
+
+export type MenuProps = {
+  items: MenuItems;
+  onItemClicked?: () => void;
+  className?: string;
+  style?: CSSProperties;
+};
+
+export default function Menu({
+  items,
+  onItemClicked,
+  className,
+  style,
+}: MenuProps) {
+  const c = useC();
+
+  return (
+    <div className={classNames("cru-menu", className)} style={style}>
+      {items.map((item, index) => {
+        if (item.type === "divider") {
+          return <hr key={index} className="cru-menu-divider" />;
+        } else {
+          const { text, color, icon, onClick } = item;
+          return (
+            <button
+              key={index}
+              className={`cru-menu-item cru-clickable-${color ?? "primary"}`}
+              onClick={() => {
+                onClick();
+                onItemClicked?.();
+              }}
+            >
+              {icon != null && <Icon color={color} icon={icon} />}
+              {c(text)}
+            </button>
+          );
+        }
+      })}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/menu/PopupMenu.css b/FrontEnd/src/components/menu/PopupMenu.css
new file mode 100644
index 00000000..149e0699
--- /dev/null
+++ b/FrontEnd/src/components/menu/PopupMenu.css
@@ -0,0 +1,7 @@
+.cru-popup-menu-menu-container {
+  z-index: 1040;
+  border-radius: 3px;
+  border: var(--cru-clickable-normal-color) 1.5px solid;
+  background-color: var(--cru-background-color);
+  overflow: hidden;
+}
diff --git a/FrontEnd/src/components/menu/PopupMenu.tsx b/FrontEnd/src/components/menu/PopupMenu.tsx
new file mode 100644
index 00000000..23a67f79
--- /dev/null
+++ b/FrontEnd/src/components/menu/PopupMenu.tsx
@@ -0,0 +1,73 @@
+import { useState, CSSProperties, ReactNode } from "react";
+import classNames from "classnames";
+import { createPortal } from "react-dom";
+import { usePopper } from "react-popper";
+
+import { useClickOutside } from "~src/utilities/hooks";
+
+import Menu, { MenuItems } from "./Menu";
+
+import { ThemeColor } from "../common";
+
+import "./PopupMenu.css";
+
+export interface PopupMenuProps {
+  color?: ThemeColor;
+  items: MenuItems;
+  children?: ReactNode;
+  containerClassName?: string;
+  containerStyle?: CSSProperties;
+}
+
+export default function PopupMenu({
+  color,
+  items,
+  children,
+  containerClassName,
+  containerStyle,
+}: PopupMenuProps) {
+  const [show, setShow] = useState<boolean>(false);
+
+  const [referenceElement, setReferenceElement] =
+    useState<HTMLDivElement | null>(null);
+  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
+    null,
+  );
+  const { styles, attributes } = usePopper(referenceElement, popperElement);
+
+  useClickOutside(popperElement, () => setShow(false), true);
+
+  return (
+    <div
+      ref={setReferenceElement}
+      className={classNames(
+        "cru-popup-menu-trigger-container",
+        containerClassName,
+      )}
+      style={containerStyle}
+      onClick={() => setShow(true)}
+    >
+      {children}
+      {show &&
+        createPortal(
+          <div
+            ref={setPopperElement}
+            className={`cru-popup-menu-menu-container cru-clickable-${
+              color ?? "primary"
+            }`}
+            style={styles.popper}
+            {...attributes.popper}
+          >
+            <Menu
+              items={items}
+              onItemClicked={() => {
+                setShow(false);
+              }}
+            />
+          </div>,
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          document.getElementById("portal")!,
+        )}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/tab/TabPages.tsx b/FrontEnd/src/components/tab/TabPages.tsx
new file mode 100644
index 00000000..6a5f4469
--- /dev/null
+++ b/FrontEnd/src/components/tab/TabPages.tsx
@@ -0,0 +1,71 @@
+import * as React from "react";
+
+import { I18nText, UiLogicError } from "~src/common";
+
+import Tabs from "./Tabs";
+
+export interface TabPage {
+  name: string;
+  text: I18nText;
+  page: React.ReactNode;
+}
+
+export interface TabPagesProps {
+  pages: TabPage[];
+  actions?: React.ReactNode;
+  dense?: boolean;
+  className?: string;
+  style?: React.CSSProperties;
+  navClassName?: string;
+  navStyle?: React.CSSProperties;
+  pageContainerClassName?: string;
+  pageContainerStyle?: React.CSSProperties;
+}
+
+const TabPages: React.FC<TabPagesProps> = ({
+  pages,
+  actions,
+  dense,
+  className,
+  style,
+  navClassName,
+  navStyle,
+  pageContainerClassName,
+  pageContainerStyle,
+}) => {
+  if (pages.length === 0) {
+    throw new UiLogicError("Page list can't be empty.");
+  }
+
+  const [tab, setTab] = React.useState<string>(pages[0].name);
+
+  const currentPage = pages.find((p) => p.name === tab);
+
+  if (currentPage == null) {
+    throw new UiLogicError("Current tab value is bad.");
+  }
+
+  return (
+    <div className={className} style={style}>
+      <Tabs
+        tabs={pages.map((page) => ({
+          name: page.name,
+          text: page.text,
+          onClick: () => {
+            setTab(page.name);
+          },
+        }))}
+        dense={dense}
+        activeTabName={tab}
+        className={navClassName}
+        style={navStyle}
+        actions={actions}
+      />
+      <div className={pageContainerClassName} style={pageContainerStyle}>
+        {currentPage.page}
+      </div>
+    </div>
+  );
+};
+
+export default TabPages;
diff --git a/FrontEnd/src/components/tab/Tabs.css b/FrontEnd/src/components/tab/Tabs.css
new file mode 100644
index 00000000..395d16a7
--- /dev/null
+++ b/FrontEnd/src/components/tab/Tabs.css
@@ -0,0 +1,33 @@
+.cru-nav {
+  border-bottom: var(--cru-primary-color) 1px solid;
+  display: flex;
+}
+
+.cru-nav-item {
+  color: var(--cru-primary-color);
+  border: var(--cru-background-2-color) 0.5px solid;
+  border-bottom: none;
+  padding: 0.5em 1.5em;
+  border-top-left-radius: 5px;
+  border-top-right-radius: 5px;
+  transition: all 0.5s;
+  cursor: pointer;
+}
+
+.cru-nav.dense .cru-nav-item {
+  padding: 0.2em 1em;
+}
+
+.cru-nav-item:hover {
+  background-color: var(--cru-background-1-color);
+}
+
+.cru-nav-item.active {
+  color: var(--cru-primary-t-color);
+  background-color: var(--cru-primary-color);
+  border-color: var(--cru-primary-color);
+}
+
+.cru-nav-action-area {
+  margin-left: auto;
+}
diff --git a/FrontEnd/src/components/tab/Tabs.tsx b/FrontEnd/src/components/tab/Tabs.tsx
new file mode 100644
index 00000000..dc8d9c01
--- /dev/null
+++ b/FrontEnd/src/components/tab/Tabs.tsx
@@ -0,0 +1,62 @@
+import * as React from "react";
+import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import classnames from "classnames";
+
+import { convertI18nText, I18nText } from "~src/common";
+
+import "./Tabs.css";
+
+export interface Tab {
+  name: string;
+  text: I18nText;
+  link?: string;
+  onClick?: () => void;
+}
+
+export interface TabsProps {
+  activeTabName?: string;
+  actions?: React.ReactNode;
+  dense?: boolean;
+  tabs: Tab[];
+  className?: string;
+  style?: React.CSSProperties;
+}
+
+export default function Tabs(props: TabsProps): React.ReactElement | null {
+  const { tabs, activeTabName, className, style, dense, actions } = props;
+
+  const { t } = useTranslation();
+
+  return (
+    <div
+      className={classnames("cru-nav", dense && "dense", className)}
+      style={style}
+    >
+      {tabs.map((tab) => {
+        const active = activeTabName === tab.name;
+        const className = classnames("cru-nav-item", active && "active");
+
+        if (tab.link != null) {
+          return (
+            <Link
+              key={tab.name}
+              to={tab.link}
+              onClick={tab.onClick}
+              className={className}
+            >
+              {convertI18nText(tab.text, t)}
+            </Link>
+          );
+        } else {
+          return (
+            <span key={tab.name} onClick={tab.onClick} className={className}>
+              {convertI18nText(tab.text, t)}
+            </span>
+          );
+        }
+      })}
+      <div className="cru-nav-action-area">{actions}</div>
+    </div>
+  );
+}
diff --git a/FrontEnd/src/components/theme-color.css b/FrontEnd/src/components/theme-color.css
new file mode 100644
index 00000000..24a7e267
--- /dev/null
+++ b/FrontEnd/src/components/theme-color.css
@@ -0,0 +1,173 @@
+/* Generated by theme-generator.ts */
+
+:root {
+  --cru-primary-color: hsl(210 100% 40%);
+  --cru-primary-1-color: hsl(210 100% 37%);
+  --cru-primary-2-color: hsl(210 100% 34%);
+  --cru-primary-on-color: hsl(210 100% 100%);
+  --cru-primary-container-color: hsl(210 100% 90%);
+  --cru-primary-container-1-color: hsl(210 100% 80%);
+  --cru-primary-container-2-color: hsl(210 100% 70%);
+  --cru-primary-on-container-color: hsl(210 100% 10%);
+  --cru-secondary-color: hsl(40 100% 40%);
+  --cru-secondary-1-color: hsl(40 100% 37%);
+  --cru-secondary-2-color: hsl(40 100% 34%);
+  --cru-secondary-on-color: hsl(40 100% 100%);
+  --cru-secondary-container-color: hsl(40 100% 90%);
+  --cru-secondary-container-1-color: hsl(40 100% 80%);
+  --cru-secondary-container-2-color: hsl(40 100% 70%);
+  --cru-secondary-on-container-color: hsl(40 100% 10%);
+  --cru-tertiary-color: hsl(160 100% 40%);
+  --cru-tertiary-1-color: hsl(160 100% 37%);
+  --cru-tertiary-2-color: hsl(160 100% 34%);
+  --cru-tertiary-on-color: hsl(160 100% 100%);
+  --cru-tertiary-container-color: hsl(160 100% 90%);
+  --cru-tertiary-container-1-color: hsl(160 100% 80%);
+  --cru-tertiary-container-2-color: hsl(160 100% 70%);
+  --cru-tertiary-on-container-color: hsl(160 100% 10%);
+  --cru-danger-color: hsl(0 100% 40%);
+  --cru-danger-1-color: hsl(0 100% 37%);
+  --cru-danger-2-color: hsl(0 100% 34%);
+  --cru-danger-on-color: hsl(0 100% 100%);
+  --cru-danger-container-color: hsl(0 100% 90%);
+  --cru-danger-container-1-color: hsl(0 100% 80%);
+  --cru-danger-container-2-color: hsl(0 100% 70%);
+  --cru-danger-on-container-color: hsl(0 100% 10%);
+  --cru-success-color: hsl(120 60% 40%);
+  --cru-success-1-color: hsl(120 60% 37%);
+  --cru-success-2-color: hsl(120 60% 34%);
+  --cru-success-on-color: hsl(120 60% 100%);
+  --cru-success-container-color: hsl(120 60% 90%);
+  --cru-success-container-1-color: hsl(120 60% 80%);
+  --cru-success-container-2-color: hsl(120 60% 70%);
+  --cru-success-on-container-color: hsl(120 60% 10%);
+  --cru-surface-dim-color: hsl(0 0% 87%);
+  --cru-surface-color: hsl(0 0% 98%);
+  --cru-surface-1-color: hsl(0 0% 90%);
+  --cru-surface-2-color: hsl(0 0% 82%);
+  --cru-surface-bright-color: hsl(0 0% 98%);
+  --cru-surface-container-lowest-color: hsl(0 0% 100%);
+  --cru-surface-container-low-color: hsl(0 0% 96%);
+  --cru-surface-container-color: hsl(0 0% 94%);
+  --cru-surface-container-high-color: hsl(0 0% 92%);
+  --cru-surface-container-highest-color: hsl(0 0% 90%);
+  --cru-surface-on-color: hsl(0 0% 10%);
+  --cru-surface-on-variant-color: hsl(0 0% 30%);
+  --cru-surface-outline-color: hsl(0 0% 50%);
+  --cru-surface-outline-variant-color: hsl(0 0% 80%);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --cru-primary-color: hsl(210 100% 80%);
+    --cru-primary-1-color: hsl(210 100% 75%);
+    --cru-primary-2-color: hsl(210 100% 68%);
+    --cru-primary-on-color: hsl(210 100% 20%);
+    --cru-primary-container-color: hsl(210 100% 30%);
+    --cru-primary-container-1-color: hsl(210 100% 25%);
+    --cru-primary-container-2-color: hsl(210 100% 20%);
+    --cru-primary-on-container-color: hsl(210 100% 90%);
+    --cru-secondary-color: hsl(40 100% 80%);
+    --cru-secondary-1-color: hsl(40 100% 75%);
+    --cru-secondary-2-color: hsl(40 100% 68%);
+    --cru-secondary-on-color: hsl(40 100% 20%);
+    --cru-secondary-container-color: hsl(40 100% 30%);
+    --cru-secondary-container-1-color: hsl(40 100% 25%);
+    --cru-secondary-container-2-color: hsl(40 100% 20%);
+    --cru-secondary-on-container-color: hsl(40 100% 90%);
+    --cru-tertiary-color: hsl(160 100% 80%);
+    --cru-tertiary-1-color: hsl(160 100% 75%);
+    --cru-tertiary-2-color: hsl(160 100% 68%);
+    --cru-tertiary-on-color: hsl(160 100% 20%);
+    --cru-tertiary-container-color: hsl(160 100% 30%);
+    --cru-tertiary-container-1-color: hsl(160 100% 25%);
+    --cru-tertiary-container-2-color: hsl(160 100% 20%);
+    --cru-tertiary-on-container-color: hsl(160 100% 90%);
+    --cru-danger-color: hsl(0 100% 80%);
+    --cru-danger-1-color: hsl(0 100% 75%);
+    --cru-danger-2-color: hsl(0 100% 68%);
+    --cru-danger-on-color: hsl(0 100% 20%);
+    --cru-danger-container-color: hsl(0 100% 30%);
+    --cru-danger-container-1-color: hsl(0 100% 25%);
+    --cru-danger-container-2-color: hsl(0 100% 20%);
+    --cru-danger-on-container-color: hsl(0 100% 90%);
+    --cru-success-color: hsl(120 60% 80%);
+    --cru-success-1-color: hsl(120 60% 75%);
+    --cru-success-2-color: hsl(120 60% 68%);
+    --cru-success-on-color: hsl(120 60% 20%);
+    --cru-success-container-color: hsl(120 60% 30%);
+    --cru-success-container-1-color: hsl(120 60% 25%);
+    --cru-success-container-2-color: hsl(120 60% 20%);
+    --cru-success-on-container-color: hsl(120 60% 90%);
+    --cru-surface-dim-color: hsl(0 0% 6%);
+    --cru-surface-color: hsl(0 0% 6%);
+    --cru-surface-1-color: hsl(0 0% 25%);
+    --cru-surface-2-color: hsl(0 0% 40%);
+    --cru-surface-bright-color: hsl(0 0% 24%);
+    --cru-surface-container-lowest-color: hsl(0 0% 4%);
+    --cru-surface-container-low-color: hsl(0 0% 10%);
+    --cru-surface-container-color: hsl(0 0% 12%);
+    --cru-surface-container-high-color: hsl(0 0% 17%);
+    --cru-surface-container-highest-color: hsl(0 0% 22%);
+    --cru-surface-on-color: hsl(0 0% 90%);
+    --cru-surface-on-variant-color: hsl(0 0% 80%);
+    --cru-surface-outline-color: hsl(0 0% 60%);
+    --cru-surface-outline-variant-color: hsl(0 0% 30%);
+  }
+}
+
+.cru-primary {
+  --cru-key-color: var(--cru-primary-color);
+  --cru-key-1-color: var(--cru-primary-1-color);
+  --cru-key-2-color: var(--cru-primary-2-color);
+  --cru-key-on-color: var(--cru-primary-on-color);
+  --cru-key-container-color: var(--cru-primary-container-color);
+  --cru-key-container-1-color: var(--cru-primary-container-1-color);
+  --cru-key-container-2-color: var(--cru-primary-container-2-color);
+  --cru-key-on-container-color: var(--cru-primary-on-container-color);
+}
+
+.cru-secondary {
+  --cru-key-color: var(--cru-secondary-color);
+  --cru-key-1-color: var(--cru-secondary-1-color);
+  --cru-key-2-color: var(--cru-secondary-2-color);
+  --cru-key-on-color: var(--cru-secondary-on-color);
+  --cru-key-container-color: var(--cru-secondary-container-color);
+  --cru-key-container-1-color: var(--cru-secondary-container-1-color);
+  --cru-key-container-2-color: var(--cru-secondary-container-2-color);
+  --cru-key-on-container-color: var(--cru-secondary-on-container-color);
+}
+
+.cru-tertiary {
+  --cru-key-color: var(--cru-tertiary-color);
+  --cru-key-1-color: var(--cru-tertiary-1-color);
+  --cru-key-2-color: var(--cru-tertiary-2-color);
+  --cru-key-on-color: var(--cru-tertiary-on-color);
+  --cru-key-container-color: var(--cru-tertiary-container-color);
+  --cru-key-container-1-color: var(--cru-tertiary-container-1-color);
+  --cru-key-container-2-color: var(--cru-tertiary-container-2-color);
+  --cru-key-on-container-color: var(--cru-tertiary-on-container-color);
+}
+
+.cru-danger {
+  --cru-key-color: var(--cru-danger-color);
+  --cru-key-1-color: var(--cru-danger-1-color);
+  --cru-key-2-color: var(--cru-danger-2-color);
+  --cru-key-on-color: var(--cru-danger-on-color);
+  --cru-key-container-color: var(--cru-danger-container-color);
+  --cru-key-container-1-color: var(--cru-danger-container-1-color);
+  --cru-key-container-2-color: var(--cru-danger-container-2-color);
+  --cru-key-on-container-color: var(--cru-danger-on-container-color);
+}
+
+.cru-success {
+  --cru-key-color: var(--cru-success-color);
+  --cru-key-1-color: var(--cru-success-1-color);
+  --cru-key-2-color: var(--cru-success-2-color);
+  --cru-key-on-color: var(--cru-success-on-color);
+  --cru-key-container-color: var(--cru-success-container-color);
+  --cru-key-container-1-color: var(--cru-success-container-1-color);
+  --cru-key-container-2-color: var(--cru-success-container-2-color);
+  --cru-key-on-container-color: var(--cru-success-on-container-color);
+}
+
diff --git a/FrontEnd/src/components/theme.css b/FrontEnd/src/components/theme.css
new file mode 100644
index 00000000..6ceb369f
--- /dev/null
+++ b/FrontEnd/src/components/theme.css
@@ -0,0 +1,146 @@
+@import "./theme-color.css";
+
+:root {
+  --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  --cru-page-padding: 1em 2em;
+
+  --cru-border-radius: 4px;
+  --cru-card-border-radius: 4px;
+}
+
+/* theme colors */
+:root {
+  --cru-primary-color: hsl(210, 100%, 50%);
+  --cru-secondary-color: hsl(30, 100%, 50%);
+  --cru-create-color: hsl(120, 100%, 25%);
+  --cru-danger-color: hsl(0, 100%, 50%);
+}
+
+/* common colors */
+:root {
+  --cru-background-color: hsl(0, 0%, 100%);
+  --cru-container-background-color: hsl(0, 0%, 97%);
+  --cru-text-primary-color: hsl(0, 0%, 0%);
+  --cru-text-secondary-color: hsl(0, 0%, 38%);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --cru-background-color: hsl(0, 0%, 0%);
+    --cru-container-background-color: hsl(0, 0%, 2%);
+    --cru-text-primary-color: hsl(0, 0%, 100%);
+    --cru-text-secondary-color: hsl(0, 0%, 85%);
+  }
+}
+
+:root {
+  --cru-body-background-color: var(--cru-background-color);
+}
+
+/* clickable color */
+:root {
+  --cru-clickable-primary-normal-color: var(--cru-primary-color);
+  --cru-clickable-primary-hover-color: hsl(210, 100%, 60%);
+  --cru-clickable-primary-focus-color: hsl(210, 100%, 60%);
+  --cru-clickable-primary-active-color: hsl(210, 100%, 70%);
+  --cru-clickable-secondary-normal-color: var(--cru-secondary-color);
+  --cru-clickable-secondary-hover-color: hsl(30, 100%, 60%);
+  --cru-clickable-secondary-focus-color: hsl(30, 100%, 60%);
+  --cru-clickable-secondary-active-color: hsl(30, 100%, 70%);
+  --cru-clickable-create-normal-color: var(--cru-create-color);
+  --cru-clickable-create-hover-color: hsl(120, 100%, 35%);
+  --cru-clickable-create-focus-color: hsl(120, 100%, 35%);
+  --cru-clickable-create-active-color: hsl(120, 100%, 35%);
+  --cru-clickable-danger-normal-color: var(--cru-danger-color);
+  --cru-clickable-danger-hover-color: hsl(0, 100%, 60%);
+  --cru-clickable-danger-focus-color: hsl(0, 100%, 60%);
+  --cru-clickable-danger-active-color: hsl(0, 100%, 70%);
+  --cru-clickable-grayscale-normal-color: hsl(0, 0%, 100%);
+  --cru-clickable-grayscale-hover-color: hsl(0, 0%, 92%);
+  --cru-clickable-grayscale-focus-color: hsl(0, 0%, 92%);
+  --cru-clickable-grayscale-active-color: hsl(0, 0%, 88%);
+  --cru-clickable-disabled-color: hsl(0, 0%, 50%);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --cru-clickable-grayscale-normal-color: hsl(0, 0%, 0%);
+    --cru-clickable-grayscale-hover-color: hsl(0, 0%, 10%);
+    --cru-clickable-grayscale-focus-color: hsl(0, 0%, 10%);
+    --cru-clickable-grayscale-active-color: hsl(0, 0%, 20%);
+  }
+}
+
+.cru-clickable-primary {
+  --cru-clickable-normal-color: var(--cru-clickable-primary-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-primary-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-primary-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-primary-active-color);
+}
+
+.cru-clickable-secondary {
+  --cru-clickable-normal-color: var(--cru-clickable-secondary-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-secondary-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-secondary-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-secondary-active-color);
+}
+
+.cru-clickable-create {
+  --cru-clickable-normal-color: var(--cru-clickable-create-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-create-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-create-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-create-active-color);
+}
+
+.cru-clickable-danger {
+  --cru-clickable-normal-color: var(--cru-clickable-danger-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-danger-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-danger-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-danger-active-color);
+}
+
+.cru-clickable-grayscale {
+  --cru-clickable-normal-color: var(--cru-clickable-grayscale-normal-color);
+  --cru-clickable-hover-color: var(--cru-clickable-grayscale-hover-color);
+  --cru-clickable-focus-color: var(--cru-clickable-grayscale-focus-color);
+  --cru-clickable-active-color: var(--cru-clickable-grayscale-active-color);
+}
+
+/* button colors */
+:root {
+  /* push button colors */
+  --cru-push-button-text-color: #ffffff;
+  --cru-push-button-disabled-text-color: hsl(0, 0%, 80%);
+}
+
+/* Card colors */
+:root {
+  --cru-card-background-primary-color: hsl(210, 100%, 50%);
+  --cru-card-border-primary-color: hsl(210, 100%, 50%);
+  --cru-card-background-secondary-color: hsl(30, 100%, 50%);
+  --cru-card-border-secondary-color: hsl(30, 100%, 50%);
+  --cru-card-background-create-color: hsl(120, 100%, 25%);
+  --cru-card-border-create-color: hsl(120, 100%, 25%);
+  --cru-card-background-danger-color: hsl(0, 100%, 50%);
+  --cru-card-border-danger-color: hsl(0, 100%, 50%);
+}
+
+.cru-card-primary {
+  --cru-card-background-color: var(--cru-card-background-primary-color);
+  --cru-card-border-color: var(--cru-card-border-primary-color)
+}
+
+.cru-card-secondary {
+  --cru-card-background-color: var(--cru-card-background-secondary-color);
+  --cru-card-border-color: var(--cru-card-border-secondary-color)
+}
+
+.cru-card-create {
+  --cru-card-background-color: var(--cru-card-background-create-color);
+  --cru-card-border-color: var(--cru-card-border-create-color)
+}
+
+.cru-card-danger {
+  --cru-card-background-color: var(--cru-card-background-danger-color);
+  --cru-card-border-color: var(--cru-card-border-danger-color)
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/user/UserAvatar.tsx b/FrontEnd/src/components/user/UserAvatar.tsx
new file mode 100644
index 00000000..8671f2d8
--- /dev/null
+++ b/FrontEnd/src/components/user/UserAvatar.tsx
@@ -0,0 +1,22 @@
+import { Ref, ComponentPropsWithoutRef } from "react";
+
+import { getHttpUserClient } from "~src/http/user";
+
+export interface UserAvatarProps extends ComponentPropsWithoutRef<"img"> {
+  username: string;
+  imgRef?: Ref<HTMLImageElement> | null;
+}
+
+export default function UserAvatar({
+  username,
+  imgRef,
+  ...otherProps
+}: UserAvatarProps) {
+  return (
+    <img
+      ref={imgRef}
+      src={getHttpUserClient().generateAvatarUrl(username)}
+      {...otherProps}
+    />
+  );
+}
diff --git a/FrontEnd/src/http/bookmark.ts b/FrontEnd/src/http/bookmark.ts
index 40e121cc..311f9a0f 100644
--- a/FrontEnd/src/http/bookmark.ts
+++ b/FrontEnd/src/http/bookmark.ts
@@ -1,4 +1,4 @@
-import { withQuery } from "@/utilities/url";
+import { withQuery } from "~src/utilities/url";
 
 import { axios, apiBaseUrl, extractResponseData, Page } from "./common";
 
diff --git a/FrontEnd/src/http/timeline.ts b/FrontEnd/src/http/timeline.ts
index 401ae116..255c786e 100644
--- a/FrontEnd/src/http/timeline.ts
+++ b/FrontEnd/src/http/timeline.ts
@@ -1,4 +1,4 @@
-import { withQuery } from "@/utilities/url";
+import { withQuery } from "~src/utilities/url";
 
 import {
   axios,
diff --git a/FrontEnd/src/migrating/center/CenterBoards.tsx b/FrontEnd/src/migrating/center/CenterBoards.tsx
index a8be2c29..f1c3fc6a 100644
--- a/FrontEnd/src/migrating/center/CenterBoards.tsx
+++ b/FrontEnd/src/migrating/center/CenterBoards.tsx
@@ -1,13 +1,13 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
 
-import { highlightTimelineUsername } from "@/common";
+import { highlightTimelineUsername } from "~src/common";
 
-import { pushAlert } from "@/services/alert";
-import { useUserLoggedIn } from "@/services/user";
+import { pushAlert } from "~src/services/alert";
+import { useUserLoggedIn } from "~src/services/user";
 
-import { getHttpTimelineClient } from "@/http/timeline";
-import { getHttpBookmarkClient } from "@/http/bookmark";
+import { getHttpTimelineClient } from "~src/http/timeline";
+import { getHttpBookmarkClient } from "~src/http/bookmark";
 
 import TimelineBoard from "./TimelineBoard";
 
diff --git a/FrontEnd/src/migrating/center/TimelineBoard.tsx b/FrontEnd/src/migrating/center/TimelineBoard.tsx
index b3ccdf8c..8f4401bc 100644
--- a/FrontEnd/src/migrating/center/TimelineBoard.tsx
+++ b/FrontEnd/src/migrating/center/TimelineBoard.tsx
@@ -2,7 +2,7 @@ import * as React from "react";
 import classnames from "classnames";
 import { Link } from "react-router-dom";
 
-import { TimelineBookmark } from "@/http/bookmark";
+import { TimelineBookmark } from "~src/http/bookmark";
 
 import TimelineLogo from "../common/TimelineLogo";
 import LoadFailReload from "../common/LoadFailReload";
diff --git a/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx
index 63742936..340a08fe 100644
--- a/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx
+++ b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx
@@ -1,11 +1,11 @@
 import * as React from "react";
 import { useNavigate } from "react-router-dom";
 
-import { validateTimelineName } from "@/services/timeline";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
+import { validateTimelineName } from "~src/services/timeline";
+import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline";
 
 import OperationDialog from "../common/dialog/OperationDialog";
-import { useUserLoggedIn } from "@/services/user";
+import { useUserLoggedIn } from "~src/services/user";
 
 interface TimelineCreateDialogProps {
   open: boolean;
diff --git a/FrontEnd/src/migrating/center/index.tsx b/FrontEnd/src/migrating/center/index.tsx
index 77af2c20..11502517 100644
--- a/FrontEnd/src/migrating/center/index.tsx
+++ b/FrontEnd/src/migrating/center/index.tsx
@@ -1,7 +1,7 @@
 import * as React from "react";
 import { useNavigate } from "react-router-dom";
 
-import { useUserLoggedIn } from "@/services/user";
+import { useUserLoggedIn } from "~src/services/user";
 
 import SearchInput from "../common/SearchInput";
 import Button from "../common/button/Button";
diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx
index acec1735..bce64322 100644
--- a/FrontEnd/src/pages/about/index.tsx
+++ b/FrontEnd/src/pages/about/index.tsx
@@ -1,7 +1,7 @@
 import "./index.css";
 
-import { useC } from "@/common";
-import Page from "@/views/common/Page";
+import { useC } from "~src/common";
+import Page from "~src/components/Page";
 
 interface Credit {
   name: string;
diff --git a/FrontEnd/src/pages/loading/index.tsx b/FrontEnd/src/pages/loading/index.tsx
index e4c8edab..29d27adc 100644
--- a/FrontEnd/src/pages/loading/index.tsx
+++ b/FrontEnd/src/pages/loading/index.tsx
@@ -1,4 +1,4 @@
-import Spinner from "@/views/common/Spinner";
+import Spinner from "~src/components/Spinner";
 
 import "./index.css";
 
diff --git a/FrontEnd/src/pages/login/index.tsx b/FrontEnd/src/pages/login/index.tsx
index a09e32c3..582ebd0f 100644
--- a/FrontEnd/src/pages/login/index.tsx
+++ b/FrontEnd/src/pages/login/index.tsx
@@ -2,16 +2,16 @@ import { useState, useEffect } from "react";
 import { Link, useNavigate } from "react-router-dom";
 import { Trans } from "react-i18next";
 
-import { useUser, userService } from "@/services/user";
+import { useUser, userService } from "~src/services/user";
 
-import { useC } from "@/views/common/common";
-import LoadingButton from "@/views/common/button/LoadingButton";
+import { useC } from "~src/components/common";
+import LoadingButton from "~src/components/button/LoadingButton";
 import {
   InputErrorDict,
   InputGroup,
   useInputs,
-} from "@/views/common/input/InputGroup";
-import Page from "@/views/common/Page";
+} from "~src/components/input/InputGroup";
+import Page from "~src/components/Page";
 
 import "./index.css";
 
diff --git a/FrontEnd/src/pages/register/index.tsx b/FrontEnd/src/pages/register/index.tsx
index bc474adb..9e478612 100644
--- a/FrontEnd/src/pages/register/index.tsx
+++ b/FrontEnd/src/pages/register/index.tsx
@@ -2,16 +2,16 @@ import { useState, useEffect } from "react";
 import { useTranslation } from "react-i18next";
 import { useNavigate } from "react-router-dom";
 
-import { HttpBadRequestError } from "@/http/common";
-import { getHttpTokenClient } from "@/http/token";
-import { userService, useUser } from "@/services/user";
+import { HttpBadRequestError } from "~src/http/common";
+import { getHttpTokenClient } from "~src/http/token";
+import { userService, useUser } from "~src/services/user";
 
-import { LoadingButton } from "@/views/common/button";
+import { LoadingButton } from "~src/components/button";
 import {
   useInputs,
   InputErrorDict,
   InputGroup,
-} from "@/views/common/input/InputGroup";
+} from "~src/components/input/InputGroup";
 
 import "./index.css";
 
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
index f35fc5a7..c34bcf4f 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
@@ -1,19 +1,19 @@
 import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react";
 
-import { useC, Text, UiLogicError } from "@/common";
+import { useC, Text, UiLogicError } from "~src/common";
 
-import { useUser } from "@/services/user";
+import { useUser } from "~src/services/user";
 
-import { getHttpUserClient } from "@/http/user";
+import { getHttpUserClient } from "~src/http/user";
 
 import ImageCropper, {
   Clip,
   applyClipToImage,
-} from "@/views/common/ImageCropper";
-import BlobImage from "@/views/common/BlobImage";
-import ButtonRowV2 from "@/views/common/button/ButtonRowV2";
-import Dialog from "@/views/common/dialog/Dialog";
-import DialogContainer from "@/views/common/dialog/DialogContainer";
+} from "~src/components/ImageCropper";
+import BlobImage from "~src/components/BlobImage";
+import ButtonRowV2 from "~src/components/button/ButtonRowV2";
+import Dialog from "~src/components/dialog/Dialog";
+import DialogContainer from "~src/components/dialog/DialogContainer";
 
 import "./ChangeAvatarDialog.css";
 
diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
index 4d318543..843659ef 100644
--- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
@@ -1,7 +1,7 @@
-import { getHttpUserClient } from "@/http/user";
-import { useUserLoggedIn } from "@/services/user";
+import { getHttpUserClient } from "~src/http/user";
+import { useUserLoggedIn } from "~src/services/user";
 
-import OperationDialog from "@/views/common/dialog/OperationDialog";
+import OperationDialog from "~src/components/dialog/OperationDialog";
 
 export interface ChangeNicknameDialogProps {
   open: boolean;
@@ -26,7 +26,7 @@ export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) {
       ]}
       onProcess={({ newNickname }) => {
         return getHttpUserClient().patch(user.username, {
-          nickname: newNickname as string,
+          nickname: newNickname,
         });
       }}
       onClose={onClose}
diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
index 87a970a5..bfcea92d 100644
--- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
@@ -1,11 +1,11 @@
 import { useState } from "react";
 import { useNavigate } from "react-router-dom";
 
-import { userService } from "@/services/user";
+import { userService } from "~src/services/user";
 
 import OperationDialog, {
   InputErrorDict,
-} from "@/views/common/dialog/OperationDialog";
+} from "~src/components/dialog/OperationDialog";
 
 interface ChangePasswordDialogProps {
   open: boolean;
@@ -65,10 +65,7 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) {
         },
       }}
       onProcess={async ({ oldPassword, newPassword }) => {
-        await userService.changePassword(
-          oldPassword as string,
-          newPassword as string,
-        );
+        await userService.changePassword(oldPassword, newPassword);
         setRedirect(true);
       }}
       onSuccessAndClose={() => {
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx
index 50967a3c..67416a08 100644
--- a/FrontEnd/src/pages/setting/index.tsx
+++ b/FrontEnd/src/pages/setting/index.tsx
@@ -8,21 +8,21 @@ import { useTranslation } from "react-i18next";
 import { useNavigate } from "react-router-dom";
 import classNames from "classnames";
 
-import { useC, Text } from "@/common";
-import { useUser, userService } from "@/services/user";
-import { getHttpUserClient } from "@/http/user";
+import { useC, Text } from "~src/common";
+import { useUser, userService } from "~src/services/user";
+import { getHttpUserClient } from "~src/http/user";
 
-import { useDialog } from "@/views/common/dialog";
-import ConfirmDialog from "@/views/common/dialog/ConfirmDialog";
-import Card from "@/views/common/Card";
-import Spinner from "@/views/common/Spinner";
-import Page from "@/views/common/Page";
+import { useDialog } from "~src/components/dialog";
+import ConfirmDialog from "~src/components/dialog/ConfirmDialog";
+import Card from "~src/components/Card";
+import Spinner from "~src/components/Spinner";
+import Page from "~src/components/Page";
 import ChangePasswordDialog from "./ChangePasswordDialog";
 import ChangeAvatarDialog from "./ChangeAvatarDialog";
 import ChangeNicknameDialog from "./ChangeNicknameDialog";
 
 import "./index.css";
-import { pushAlert } from "@/services/alert";
+import { pushAlert } from "~src/services/alert";
 
 interface SettingSectionProps
   extends Omit<ComponentPropsWithoutRef<typeof Card>, "title"> {
diff --git a/FrontEnd/src/pages/timeline/CollapseButton.tsx b/FrontEnd/src/pages/timeline/CollapseButton.tsx
index 14fc6bee..1c4fa2ba 100644
--- a/FrontEnd/src/pages/timeline/CollapseButton.tsx
+++ b/FrontEnd/src/pages/timeline/CollapseButton.tsx
@@ -1,6 +1,6 @@
 import { CSSProperties } from "react";
 
-import IconButton from "@/views/common/button/IconButton";
+import IconButton from "~src/components/button/IconButton";
 
 export default function CollapseButton({
   collapse,
diff --git a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx
index 9c497108..43e81d67 100644
--- a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx
+++ b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx
@@ -2,15 +2,15 @@ import * as React from "react";
 import classnames from "classnames";
 import { useTranslation } from "react-i18next";
 
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline";
 
-import TimelinePostBuilder from "@/services/TimelinePostBuilder";
+import TimelinePostBuilder from "~src/services/TimelinePostBuilder";
 
-import FlatButton from "@/views/common/button/FlatButton";
-import TabPages from "@/views/common/tab/TabPages";
-import ConfirmDialog from "@/views/common/dialog/ConfirmDialog";
-import Spinner from "@/views/common/Spinner";
-import IconButton from "@/views/common/button/IconButton";
+import FlatButton from "~src/components/button/FlatButton";
+import TabPages from "~src/components/tab/TabPages";
+import ConfirmDialog from "~src/components/dialog/ConfirmDialog";
+import Spinner from "~src/components/Spinner";
+import IconButton from "~src/components/button/IconButton";
 
 import "./MarkdownPostEdit.css";
 
diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx
index 73e621c1..f266ec9d 100644
--- a/FrontEnd/src/pages/timeline/Timeline.tsx
+++ b/FrontEnd/src/pages/timeline/Timeline.tsx
@@ -1,20 +1,20 @@
 import { useState, useEffect } from "react";
 import classnames from "classnames";
-import { useScrollToBottom } from "@/utilities/hooks";
+import { useScrollToBottom } from "~src/utilities/hooks";
 import { HubConnectionState } from "@microsoft/signalr";
 
 import {
   HttpForbiddenError,
   HttpNetworkError,
   HttpNotFoundError,
-} from "@/http/common";
+} from "~src/http/common";
 import {
   getHttpTimelineClient,
   HttpTimelineInfo,
   HttpTimelinePostInfo,
-} from "@/http/timeline";
+} from "~src/http/timeline";
 
-import { getTimelinePostUpdate$ } from "@/services/timeline";
+import { getTimelinePostUpdate$ } from "~src/services/timeline";
 
 import TimelinePostList from "./TimelinePostList";
 import TimelinePostEdit from "./TimelinePostCreateView";
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx
index 2987aa74..82d6d350 100644
--- a/FrontEnd/src/pages/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx
@@ -1,24 +1,24 @@
 import { useState } from "react";
 import { HubConnectionState } from "@microsoft/signalr";
 
-import { useUser } from "@/services/user";
-import { pushAlert } from "@/services/alert";
+import { useUser } from "~src/services/user";
+import { pushAlert } from "~src/services/alert";
 
-import { HttpTimelineInfo } from "@/http/timeline";
-import { getHttpBookmarkClient } from "@/http/bookmark";
+import { HttpTimelineInfo } from "~src/http/timeline";
+import { getHttpBookmarkClient } from "~src/http/bookmark";
 
-import { useMobile } from "@/views/common/common";
-import { Dialog, useDialog } from "@/views/common/dialog";
-import UserAvatar from "@/views/common/user/UserAvatar";
-import PopupMenu from "@/views/common/menu/PopupMenu";
-import FullPageDialog from "@/views/common/dialog/FullPageDialog";
-import Card from "@/views/common/Card";
+import { useMobile } from "~src/components/common";
+import { Dialog, useDialog } from "~src/components/dialog";
+import UserAvatar from "~src/components/user/UserAvatar";
+import PopupMenu from "~src/components/menu/PopupMenu";
+import FullPageDialog from "~src/components/dialog/FullPageDialog";
+import Card from "~src/components/Card";
 import TimelineDeleteDialog from "./TimelineDeleteDialog";
 import ConnectionStatusBadge from "./ConnectionStatusBadge";
 import CollapseButton from "./CollapseButton";
 import TimelineMember from "./TimelineMember";
 import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
-import IconButton from "@/views/common/button/IconButton";
+import IconButton from "~src/components/button/IconButton";
 
 import "./TimelineCard.css";
 
diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
index 7d7b9527..7b7b8e8c 100644
--- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx
@@ -2,9 +2,9 @@ import * as React from "react";
 import { useNavigate } from "react-router-dom";
 import { Trans } from "react-i18next";
 
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
+import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline";
 
-import OperationDialog from "@/views/common/dialog/OperationDialog";
+import OperationDialog from "~src/components/dialog/OperationDialog";
 
 interface TimelineDeleteDialog {
   timeline: HttpTimelineInfo;
diff --git a/FrontEnd/src/pages/timeline/TimelineMember.tsx b/FrontEnd/src/pages/timeline/TimelineMember.tsx
index 4c1600f5..a25fe6a9 100644
--- a/FrontEnd/src/pages/timeline/TimelineMember.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineMember.tsx
@@ -1,16 +1,16 @@
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
 
-import { convertI18nText, I18nText } from "@/common";
+import { convertI18nText, I18nText } from "~src/common";
 
-import { HttpUser } from "@/http/user";
-import { getHttpSearchClient } from "@/http/search";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
+import { HttpUser } from "~src/http/user";
+import { getHttpSearchClient } from "~src/http/search";
+import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline";
 
-import SearchInput from "@/views/common/SearchInput";
-import UserAvatar from "@/views/common/user/UserAvatar";
-import Button from "@/views/common/button/Button";
-import { ListContainer, ListItemContainer } from "@/views/common/list";
+import SearchInput from "~src/components/SearchInput";
+import UserAvatar from "~src/components/user/UserAvatar";
+import Button from "~src/components/button/Button";
+import { ListContainer, ListItemContainer } from "~src/components/list";
 
 import "./TimelineMember.css";
 
diff --git a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx
index 23dd141f..d3fd3215 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx
@@ -1,7 +1,7 @@
 import { ReactNode } from "react";
 import classNames from "classnames";
 
-import Card from "@/views/common/Card";
+import Card from "~src/components/Card";
 
 import "./TimelinePostCard.css";
 
diff --git a/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx b/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx
index ad5465c1..6c0d7387 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostContentView.tsx
@@ -2,15 +2,15 @@ import * as React from "react";
 import classnames from "classnames";
 import { marked } from "marked";
 
-import { UiLogicError } from "@/common";
+import { UiLogicError } from "~src/common";
 
-import { HttpNetworkError } from "@/http/common";
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
+import { HttpNetworkError } from "~src/http/common";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline";
 
-import { useUser } from "@/services/user";
+import { useUser } from "~src/services/user";
 
-import Skeleton from "@/views/common/Skeleton";
-import LoadFailReload from "@/views/common/LoadFailReload";
+import Skeleton from "~src/components/Skeleton";
+import LoadFailReload from "~src/components/LoadFailReload";
 
 const TextView: React.FC<TimelinePostContentViewProps> = (props) => {
   const { post, className, style } = props;
diff --git a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx
index 572a9119..3c41228a 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx
@@ -1,26 +1,26 @@
 import { useState, useEffect, ChangeEventHandler } from "react";
 import { useTranslation } from "react-i18next";
 
-import { UiLogicError } from "@/common";
+import { UiLogicError } from "~src/common";
 
 import {
   getHttpTimelineClient,
   HttpTimelineInfo,
   HttpTimelinePostInfo,
   HttpTimelinePostPostRequestData,
-} from "@/http/timeline";
+} from "~src/http/timeline";
 
-import { pushAlert } from "@/services/alert";
+import { pushAlert } from "~src/services/alert";
 
-import base64 from "@/utilities/base64";
+import base64 from "~src/utilities/base64";
 
-import BlobImage from "@/views/common/BlobImage";
-import LoadingButton from "@/views/common/button/LoadingButton";
-import PopupMenu from "@/views/common/menu/PopupMenu";
+import BlobImage from "~src/components/BlobImage";
+import LoadingButton from "~src/components/button/LoadingButton";
+import PopupMenu from "~src/components/menu/PopupMenu";
 import MarkdownPostEdit from "./MarkdownPostEdit";
 import TimelinePostCard from "./TimelinePostCard";
 import TimelinePostContainer from "./TimelinePostContainer";
-import IconButton from "@/views/common/button/IconButton";
+import IconButton from "~src/components/button/IconButton";
 
 import "./TimelinePostCreateView.css";
 import classNames from "classnames";
diff --git a/FrontEnd/src/pages/timeline/TimelinePostList.tsx b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
index a3501b33..7912260a 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostList.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
@@ -1,6 +1,6 @@
 import { useMemo, Fragment } from "react";
 
-import { HttpTimelinePostInfo } from "@/http/timeline";
+import { HttpTimelinePostInfo } from "~src/http/timeline";
 
 import TimelinePostView from "./TimelinePostView";
 import TimelineDateLabel from "./TimelineDateLabel";
diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx
index afae5033..2a8c5947 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx
@@ -1,17 +1,17 @@
 import { useState } from "react";
 
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline";
 
-import { pushAlert } from "@/services/alert";
+import { pushAlert } from "~src/services/alert";
 
-import { useClickOutside } from "@/utilities/hooks";
+import { useClickOutside } from "~src/utilities/hooks";
 
-import UserAvatar from "@/views/common/user/UserAvatar";
-import { useDialog } from "@/views/common/dialog";
-import FlatButton from "@/views/common/button/FlatButton";
-import ConfirmDialog from "@/views/common/dialog/ConfirmDialog";
+import UserAvatar from "~src/components/user/UserAvatar";
+import { useDialog } from "~src/components/dialog";
+import FlatButton from "~src/components/button/FlatButton";
+import ConfirmDialog from "~src/components/dialog/ConfirmDialog";
 import TimelinePostContentView from "./TimelinePostContentView";
-import IconButton from "@/views/common/button/IconButton";
+import IconButton from "~src/components/button/IconButton";
 
 import TimelinePostContainer from "./TimelinePostContainer";
 import TimelinePostCard from "./TimelinePostCard";
diff --git a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx
index b57135bb..afd83a5f 100644
--- a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx
@@ -6,9 +6,9 @@ import {
   HttpTimelinePatchRequest,
   kTimelineVisibilities,
   TimelineVisibility,
-} from "@/http/timeline";
+} from "~src/http/timeline";
 
-import OperationDialog from "@/views/common/dialog/OperationDialog";
+import OperationDialog from "~src/components/dialog/OperationDialog";
 
 export interface TimelinePropertyChangeDialogProps {
   open: boolean;
@@ -68,13 +68,13 @@ const TimelinePropertyChangeDialog: React.FC<
       onProcess={({ title, visibility, description }) => {
         const req: HttpTimelinePatchRequest = {};
         if (title !== timeline.title) {
-          req.title = title as string;
+          req.title = title;
         }
         if (visibility !== timeline.visibility) {
-          req.visibility = visibility as TimelineVisibility;
+          req.visibility = visibility;
         }
         if (description !== timeline.description) {
-          req.description = description as string;
+          req.description = description;
         }
         return getHttpTimelineClient()
           .patchTimeline(timeline.owner.username, timeline.nameV2, req)
diff --git a/FrontEnd/src/pages/timeline/index.tsx b/FrontEnd/src/pages/timeline/index.tsx
index 51cc37f0..6cd1ded0 100644
--- a/FrontEnd/src/pages/timeline/index.tsx
+++ b/FrontEnd/src/pages/timeline/index.tsx
@@ -1,6 +1,6 @@
 import { useParams } from "react-router-dom";
 
-import { UiLogicError } from "@/common";
+import { UiLogicError } from "~src/common";
 
 import Timeline from "./Timeline";
 
diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts
index 0440e136..919d4f55 100644
--- a/FrontEnd/src/services/TimelinePostBuilder.ts
+++ b/FrontEnd/src/services/TimelinePostBuilder.ts
@@ -1,10 +1,10 @@
 import { marked } from "marked";
 
-import { UiLogicError } from "@/common";
+import { UiLogicError } from "~src/common";
 
-import base64 from "@/utilities/base64";
+import base64 from "~src/utilities/base64";
 
-import { HttpTimelinePostPostRequest } from "@/http/timeline";
+import { HttpTimelinePostPostRequest } from "~src/http/timeline";
 
 class TimelinePostMarkedRenderer extends marked.Renderer {
   constructor(private _images: { file: File; url: string }[]) {
diff --git a/FrontEnd/src/services/alert.ts b/FrontEnd/src/services/alert.ts
index 2f66dccc..241deab1 100644
--- a/FrontEnd/src/services/alert.ts
+++ b/FrontEnd/src/services/alert.ts
@@ -1,7 +1,7 @@
 import pull from "lodash/pull";
 
-import { I18nText } from "@/common";
-import { ThemeColor } from "@/views/common/common";
+import { I18nText } from "~src/common";
+import { ThemeColor } from "~src/views/common/common";
 
 export interface AlertInfo {
   type?: ThemeColor;
diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts
index 342f803a..41a7bff0 100644
--- a/FrontEnd/src/services/timeline.ts
+++ b/FrontEnd/src/services/timeline.ts
@@ -13,8 +13,8 @@ import {
   HubConnectionState,
 } from "@microsoft/signalr";
 
-import { TimelineVisibility } from "@/http/timeline";
-import { token$ } from "@/http/common";
+import { TimelineVisibility } from "~src/http/timeline";
+import { token$ } from "~src/http/common";
 
 // cSpell:ignore onreconnected onreconnecting
 
diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts
index c89ca893..ddba4dab 100644
--- a/FrontEnd/src/services/user.ts
+++ b/FrontEnd/src/services/user.ts
@@ -2,11 +2,11 @@ import { useState, useEffect } from "react";
 import { BehaviorSubject, Observable } from "rxjs";
 import { AxiosError } from "axios";
 
-import { UiLogicError } from "@/common";
+import { UiLogicError } from "~src/common";
 
-import { setHttpToken, axios, HttpBadRequestError } from "@/http/common";
-import { getHttpTokenClient } from "@/http/token";
-import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user";
+import { setHttpToken, axios, HttpBadRequestError } from "~src/http/common";
+import { getHttpTokenClient } from "~src/http/token";
+import { getHttpUserClient, HttpUser, UserPermission } from "~src/http/user";
 
 import { pushAlert } from "./alert";
 
diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css
deleted file mode 100644
index a0d975b5..00000000
--- a/FrontEnd/src/views/common/AppBar.css
+++ /dev/null
@@ -1,87 +0,0 @@
-.app-bar {
-  height: 56px;
-  position: fixed;
-  z-index: 1030;
-  top: 0;
-  left: 0;
-  right: 0;
-  background-color: var(--cru-primary-color);
-}
-
-.app-bar {
-  display: flex;
-}
-
-.app-bar .app-bar-brand {
-  display: flex;
-  align-items: center;
-}
-
-.app-bar .app-bar-brand-icon {
-  height: 2em;
-}
-
-.app-bar .app-bar-user-area {
-  display: flex;
-  margin-left: auto;
-}
-
-.app-bar a {
-  background-color: var(--cru-primary-color);
-  color: var(--cru-push-button-text-color);
-  text-decoration: none;
-  display: flex;
-  align-items: center;
-  padding: 0 1em;
-  transition: all 0.5s;
-}
-
-.app-bar a:hover {
-  background-color: var(--cru-clickable-primary-hover-color);
-}
-
-.app-bar a:focus {
-  background-color: var(--cru-clickable-primary-focus-color);
-}
-
-.app-bar a:active {
-  background-color: var(--cru-clickable-primary-active-color);
-}
-
-/* the current page */
-.app-bar a.active {
-  background-color: var(--cru-clickable-primary-focus-color);
-}
-
-.app-bar .app-bar-avatar img {
-  width: 45px;
-  height: 45px;
-  background-color: white;
-  border-radius: 50%;
-}
-
-.app-bar.desktop .app-bar-link-area {
-  display: flex;
-}
-
-.app-bar.mobile .app-bar-link-area {
-  position: absolute;
-  z-index: -1;
-  top: 56px;
-  left: 0;
-  right: 0;
-  transition: transform 0.5s;
-}
-
-.app-bar.mobile a {
-  height: 56px;
-}
-
-.app-bar.mobile.collapse .app-bar-link-area {
-  transform: translateY(-100%);
-}
-
-.app-bar .toggler {
-  font-size: 2em;
-  margin-right: 0.5em;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx
deleted file mode 100644
index b9ea825b..00000000
--- a/FrontEnd/src/views/common/AppBar.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useState } from "react";
-import classnames from "classnames";
-import { Link, NavLink } from "react-router-dom";
-
-import { I18nText, useC, useMobile } from "./common";
-import { useUser } from "@/services/user";
-
-import TimelineLogo from "./TimelineLogo";
-import { IconButton } from "./button";
-import UserAvatar from "./user/UserAvatar";
-
-import "./AppBar.css";
-
-function AppBarNavLink({
-  link,
-  className,
-  label,
-  onClick,
-  children,
-}: {
-  link: string;
-  className?: string;
-  label?: I18nText;
-  onClick?: () => void;
-  children?: React.ReactNode;
-}) {
-  if (label != null && children != null) {
-    throw new Error("AppBarNavLink: label and children cannot be both set");
-  }
-
-  const c = useC();
-
-  return (
-    <NavLink
-      to={link}
-      className={({ isActive }) => classnames(className, isActive && "active")}
-      onClick={onClick}
-    >
-      {children != null ? children : c(label)}
-    </NavLink>
-  );
-}
-
-export default function AppBar() {
-  const isMobile = useMobile();
-
-  const [isCollapse, setIsCollapse] = useState<boolean>(true);
-  const collapse = isMobile ? () => setIsCollapse(true) : undefined;
-  const toggleCollapse = () => setIsCollapse(!isCollapse);
-
-  const user = useUser();
-  const hasAdministrationPermission = user && user.hasAdministrationPermission;
-
-  return (
-    <nav
-      className={classnames(
-        isMobile ? "mobile" : "desktop",
-        "app-bar",
-        isCollapse && "collapse",
-      )}
-    >
-      <Link to="/" className="app-bar-brand" onClick={collapse}>
-        <TimelineLogo className="app-bar-brand-icon" />
-        Timeline
-      </Link>
-
-      <div className="app-bar-link-area">
-        <AppBarNavLink
-          link="/settings"
-          label="nav.settings"
-          onClick={collapse}
-        />
-        <AppBarNavLink link="/about" label="nav.about" onClick={collapse} />
-        {hasAdministrationPermission && (
-          <AppBarNavLink
-            link="/admin"
-            label="nav.administration"
-            onClick={collapse}
-          />
-        )}
-      </div>
-
-      <div className="app-bar-user-area">
-        {user != null ? (
-          <AppBarNavLink link="/" className="app-bar-avatar" onClick={collapse}>
-            <UserAvatar username={user.username} />
-          </AppBarNavLink>
-        ) : (
-          <AppBarNavLink link="/login" label="nav.login" onClick={collapse} />
-        )}
-      </div>
-
-      {isMobile && (
-        <IconButton icon="list" className="toggler" onClick={toggleCollapse} />
-      )}
-    </nav>
-  );
-}
diff --git a/FrontEnd/src/views/common/BlobImage.tsx b/FrontEnd/src/views/common/BlobImage.tsx
deleted file mode 100644
index 259c2210..00000000
--- a/FrontEnd/src/views/common/BlobImage.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ComponentPropsWithoutRef, useState, useEffect } from "react";
-
-type BlobImageProps = Omit<ComponentPropsWithoutRef<"img">, "src"> & {
-  imgRef?: React.Ref<HTMLImageElement>;
-  src?: Blob | string | null;
-};
-
-export default function BlobImage(props: BlobImageProps) {
-  const { imgRef, src, ...otherProps } = props;
-
-  const [url, setUrl] = useState<string | null | undefined>(undefined);
-
-  useEffect(() => {
-    if (src instanceof Blob) {
-      const url = URL.createObjectURL(src);
-      setUrl(url);
-      return () => {
-        URL.revokeObjectURL(url);
-      };
-    } else {
-      setUrl(src);
-    }
-  }, [src]);
-
-  return <img ref={imgRef} {...otherProps} src={url ?? undefined} />;
-}
diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css
deleted file mode 100644
index 6d655eb9..00000000
--- a/FrontEnd/src/views/common/Card.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.cru-card {
-  border-radius: var(--cru-card-border-radius);
-  transition: all 0.3s;
-}
-
-.cru-card-background-none {
-  background-color: transparent;
-}
-
-.cru-card-background-solid {
-  background-color: var(--cru-background-color);
-}
-
-.cru-card-background-grayscale {
-  background-color: var(--cru-container-background-color);
-}
-
-.cru-card-border-color {
-  border: 2px solid var(--cru-card-border-color);
-}
diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx
deleted file mode 100644
index a8f0d3cc..00000000
--- a/FrontEnd/src/views/common/Card.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import { ThemeColor } from "./common";
-import "./Card.css";
-
-interface CardProps extends ComponentPropsWithoutRef<"div"> {
-  containerRef?: Ref<HTMLDivElement>;
-  color?: ThemeColor;
-  border?: "color" | "none";
-  background?: "color" | "solid" | "grayscale" | "none";
-}
-
-export default function Card({
-  color,
-  background,
-  border,
-  className,
-  children,
-  containerRef,
-  ...otherProps
-}: CardProps) {
-  return (
-    <div
-      ref={containerRef}
-      className={classNames(
-        "cru-card",
-        `cru-card-${color ?? "primary"}`,
-        `cru-card-border-${border ?? "color"}`,
-        `cru-card-background-${background ?? "solid"}`,
-        className,
-      )}
-      {...otherProps}
-    >
-      {children}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/Icon.css b/FrontEnd/src/views/common/Icon.css
deleted file mode 100644
index fe980d7b..00000000
--- a/FrontEnd/src/views/common/Icon.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.cru-icon {
-  font-size: 1.4rem;
-}
diff --git a/FrontEnd/src/views/common/Icon.tsx b/FrontEnd/src/views/common/Icon.tsx
deleted file mode 100644
index 2ac3a7ca..00000000
--- a/FrontEnd/src/views/common/Icon.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ComponentPropsWithoutRef } from "react";
-import classNames from "classnames";
-
-import { ThemeColor } from "./common";
-
-import "./Icon.css";
-
-interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
-  icon: string;
-  color?: ThemeColor | "on-surface";
-  size?: string | number;
-}
-
-export default function Icon(props: IconButtonProps) {
-  const { icon, color, size, style, className, ...otherProps } = props;
-
-  const colorName = color === "on-surface" ? "surface-on" : color;
-
-  return (
-    <i
-      style={size != null ? { ...style, fontSize: size } : style}
-      className={classNames(
-        colorName && `cru-${colorName}`,
-        `bi-${icon} cru-icon`,
-        className,
-      )}
-      {...otherProps}
-    />
-  );
-}
diff --git a/FrontEnd/src/views/common/ImageCropper.css b/FrontEnd/src/views/common/ImageCropper.css
deleted file mode 100644
index 2c4d0a8c..00000000
--- a/FrontEnd/src/views/common/ImageCropper.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.image-cropper-container {
-  position: relative;
-  box-sizing: border-box;
-  user-select: none;
-}
-
-.image-cropper-container img {
-  position: absolute;
-  left: 0;
-  top: 0;
-  width: 100%;
-  height: 100%;
-}
-
-.image-cropper-mask-container {
-  position: absolute;
-  left: 0;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  overflow: hidden;
-}
-
-.image-cropper-mask {
-  position: absolute;
-  box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
-  touch-action: none;
-}
-
-.image-cropper-handler {
-  position: absolute;
-  width: 26px;
-  height: 26px;
-  border: black solid 2px;
-  border-radius: 50%;
-  background: white;
-  touch-action: none;
-}
diff --git a/FrontEnd/src/views/common/ImageCropper.tsx b/FrontEnd/src/views/common/ImageCropper.tsx
deleted file mode 100644
index fcab74b0..00000000
--- a/FrontEnd/src/views/common/ImageCropper.tsx
+++ /dev/null
@@ -1,312 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import { UiLogicError } from "@/common";
-
-import "./ImageCropper.css";
-import BlobImage from "./BlobImage";
-
-export interface Clip {
-  left: number;
-  top: number;
-  width: number;
-}
-
-interface NormailizedClip extends Clip {
-  height: number;
-}
-
-interface ImageInfo {
-  width: number;
-  height: number;
-  landscape: boolean;
-  ratio: number;
-  maxClipWidth: number;
-  maxClipHeight: number;
-}
-
-interface ImageCropperSavedState {
-  clip: NormailizedClip;
-  x: number;
-  y: number;
-  pointerId: number;
-}
-
-export interface ImageCropperProps {
-  clip: Clip | null;
-  image: string | Blob;
-  onChange: (clip: Clip) => void;
-  imageElementCallback?: (element: HTMLImageElement | null) => void;
-  className?: string;
-}
-
-const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
-  const { clip, image, onChange, imageElementCallback, className } = props;
-
-  const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>(
-    null,
-  );
-  const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null);
-
-  const normalizeClip = (c: Clip | null | undefined): NormailizedClip => {
-    if (c == null) {
-      return { left: 0, top: 0, width: 0, height: 0 };
-    }
-
-    return {
-      left: c.left || 0,
-      top: c.top || 0,
-      width: c.width || 0,
-      height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0,
-    };
-  };
-
-  const c = normalizeClip(clip);
-
-  const imgElementRef = React.useRef<HTMLImageElement | null>(null);
-
-  const onImageRef = React.useCallback(
-    (e: HTMLImageElement | null) => {
-      imgElementRef.current = e;
-      if (imageElementCallback != null && e == null) {
-        imageElementCallback(null);
-      }
-    },
-    [imageElementCallback],
-  );
-
-  const onImageLoad = React.useCallback(
-    (e: React.SyntheticEvent<HTMLImageElement>) => {
-      const img = e.currentTarget;
-      const landscape = img.naturalWidth >= img.naturalHeight;
-
-      const info = {
-        width: img.naturalWidth,
-        height: img.naturalHeight,
-        landscape,
-        ratio: img.naturalHeight / img.naturalWidth,
-        maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1,
-        maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight,
-      };
-      setImageInfo(info);
-      onChange({ left: 0, top: 0, width: info.maxClipWidth });
-      if (imageElementCallback != null) {
-        imageElementCallback(img);
-      }
-    },
-    [onChange, imageElementCallback],
-  );
-
-  const onPointerDown = React.useCallback(
-    (e: React.PointerEvent) => {
-      if (oldState != null) return;
-      e.currentTarget.setPointerCapture(e.pointerId);
-      setOldState({
-        x: e.clientX,
-        y: e.clientY,
-        clip: c,
-        pointerId: e.pointerId,
-      });
-    },
-    [oldState, c],
-  );
-
-  const onPointerUp = React.useCallback(
-    (e: React.PointerEvent) => {
-      if (oldState == null || oldState.pointerId !== e.pointerId) return;
-      e.currentTarget.releasePointerCapture(e.pointerId);
-      setOldState(null);
-    },
-    [oldState],
-  );
-
-  const onPointerMove = React.useCallback(
-    (e: React.PointerEvent) => {
-      if (oldState == null) return;
-
-      const oldClip = oldState.clip;
-
-      const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
-      const { current: imgElement } = imgElementRef;
-
-      if (imgElement == null) throw new UiLogicError("Image element is null.");
-
-      const moveRatio = {
-        x: movement.x / imgElement.width,
-        y: movement.y / imgElement.height,
-      };
-
-      const newRatio = {
-        x: oldClip.left + moveRatio.x,
-        y: oldClip.top + moveRatio.y,
-      };
-      if (newRatio.x < 0) {
-        newRatio.x = 0;
-      } else if (newRatio.x > 1 - oldClip.width) {
-        newRatio.x = 1 - oldClip.width;
-      }
-      if (newRatio.y < 0) {
-        newRatio.y = 0;
-      } else if (newRatio.y > 1 - oldClip.height) {
-        newRatio.y = 1 - oldClip.height;
-      }
-
-      onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width });
-    },
-    [oldState, onChange],
-  );
-
-  const onHandlerPointerMove = React.useCallback(
-    (e: React.PointerEvent) => {
-      if (oldState == null) return;
-
-      const oldClip = oldState.clip;
-
-      const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
-      const ratio = imageInfo == null ? 1 : imageInfo.ratio;
-
-      const { current: imgElement } = imgElementRef;
-
-      if (imgElement == null) throw new UiLogicError("Image element is null.");
-
-      const moveRatio = {
-        x: movement.x / imgElement.width,
-        y: movement.x / imgElement.width / ratio,
-      };
-
-      const newRatio = {
-        x: oldClip.width + moveRatio.x,
-        y: oldClip.height + moveRatio.y,
-      };
-
-      const maxRatio = {
-        x: Math.min(1 - oldClip.left, newRatio.x),
-        y: Math.min(1 - oldClip.top, newRatio.y),
-      };
-
-      const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio);
-
-      let newWidth;
-      if (newRatio.x < 0) {
-        newWidth = 0;
-      } else if (newRatio.x > maxWidthRatio) {
-        newWidth = maxWidthRatio;
-      } else {
-        newWidth = newRatio.x;
-      }
-
-      onChange({ left: oldClip.left, top: oldClip.top, width: newWidth });
-    },
-    [imageInfo, oldState, onChange],
-  );
-
-  const toPercentage = (n: number): string => `${n}%`;
-
-  // fuck!!! I just can't find a better way to implement this in pure css
-  const containerStyle: React.CSSProperties = (() => {
-    if (imageInfo == null) {
-      return { width: "100%", paddingTop: "100%", height: 0 };
-    } else {
-      if (imageInfo.ratio > 1) {
-        return {
-          width: toPercentage(100 / imageInfo.ratio),
-          paddingTop: "100%",
-          height: 0,
-        };
-      } else {
-        return {
-          width: "100%",
-          paddingTop: toPercentage(100 * imageInfo.ratio),
-          height: 0,
-        };
-      }
-    }
-  })();
-
-  return (
-    <div
-      className={classnames("image-cropper-container", className)}
-      style={containerStyle}
-    >
-      <BlobImage
-        imgRef={onImageRef}
-        src={image}
-        onLoad={onImageLoad}
-        alt="to crop"
-      />
-      <div className="image-cropper-mask-container">
-        <div
-          className="image-cropper-mask"
-          style={{
-            left: toPercentage(c.left * 100),
-            top: toPercentage(c.top * 100),
-            width: toPercentage(c.width * 100),
-            height: toPercentage(c.height * 100),
-          }}
-          onPointerMove={onPointerMove}
-          onPointerDown={onPointerDown}
-          onPointerUp={onPointerUp}
-        />
-      </div>
-      <div
-        className="image-cropper-handler"
-        style={{
-          left: `calc(${(c.left + c.width) * 100}% - 15px)`,
-          top: `calc(${(c.top + c.height) * 100}% - 15px)`,
-        }}
-        onPointerMove={onHandlerPointerMove}
-        onPointerDown={onPointerDown}
-        onPointerUp={onPointerUp}
-      />
-    </div>
-  );
-};
-
-export default ImageCropper;
-
-export function applyClipToImage(
-  image: HTMLImageElement,
-  clip: Clip,
-  mimeType: string,
-): Promise<Blob> {
-  return new Promise((resolve, reject) => {
-    const naturalSize = {
-      width: image.naturalWidth,
-      height: image.naturalHeight,
-    };
-    const clipArea = {
-      x: naturalSize.width * clip.left,
-      y: naturalSize.height * clip.top,
-      length: naturalSize.width * clip.width,
-    };
-
-    const canvas = document.createElement("canvas");
-    canvas.width = clipArea.length;
-    canvas.height = clipArea.length;
-    const context = canvas.getContext("2d");
-
-    if (context == null) throw new Error("Failed to create context.");
-
-    context.drawImage(
-      image,
-      clipArea.x,
-      clipArea.y,
-      clipArea.length,
-      clipArea.length,
-      0,
-      0,
-      clipArea.length,
-      clipArea.length,
-    );
-
-    canvas.toBlob((blob) => {
-      if (blob == null) {
-        reject(new Error("canvas.toBlob returns null"));
-      } else {
-        resolve(blob);
-      }
-    }, mimeType);
-  });
-}
diff --git a/FrontEnd/src/views/common/LoadFailReload.tsx b/FrontEnd/src/views/common/LoadFailReload.tsx
deleted file mode 100644
index 81ba1f67..00000000
--- a/FrontEnd/src/views/common/LoadFailReload.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import * as React from "react";
-import { Trans } from "react-i18next";
-
-export interface LoadFailReloadProps {
-  className?: string;
-  style?: React.CSSProperties;
-  onReload: () => void;
-}
-
-const LoadFailReload: React.FC<LoadFailReloadProps> = ({
-  onReload,
-  className,
-  style,
-}) => {
-  return (
-    <Trans
-      i18nKey="loadFailReload"
-      parent="div"
-      className={className}
-      style={style}
-    >
-      0
-      <a
-        href="#"
-        onClick={(e) => {
-          onReload();
-          e.preventDefault();
-        }}
-      >
-        1
-      </a>
-      2
-    </Trans>
-  );
-};
-
-export default LoadFailReload;
diff --git a/FrontEnd/src/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx
deleted file mode 100644
index 35ee1aa8..00000000
--- a/FrontEnd/src/views/common/LoadingPage.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as React from "react";
-
-import Spinner from "./Spinner";
-
-const LoadingPage: React.FC = () => {
-  return (
-    <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center">
-      <Spinner />
-    </div>
-  );
-};
-
-export default LoadingPage;
diff --git a/FrontEnd/src/views/common/Page.tsx b/FrontEnd/src/views/common/Page.tsx
deleted file mode 100644
index 86fdb2f5..00000000
--- a/FrontEnd/src/views/common/Page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-interface PageProps extends ComponentPropsWithoutRef<"div"> {
-  noTopPadding?: boolean;
-  pageRef?: Ref<HTMLDivElement>;
-}
-
-export default function Page({ noTopPadding, pageRef, className, children }: PageProps) {
-  return (
-    <div ref={pageRef} className={classNames(className, "cru-page", noTopPadding && "cru-page-no-top-padding")}>
-      {children}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/SearchInput.css b/FrontEnd/src/views/common/SearchInput.css
deleted file mode 100644
index f0503016..00000000
--- a/FrontEnd/src/views/common/SearchInput.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.cru-search-input {
-  display: flex;
-  flex-wrap: wrap;
-}
-
-.cru-search-input-input {
-  width: 100%;
-}
diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx
deleted file mode 100644
index e3216b86..00000000
--- a/FrontEnd/src/views/common/SearchInput.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import classNames from "classnames";
-
-import { useC, Text } from "./common";
-import LoadingButton from "./button/LoadingButton";
-
-import "./SearchInput.css";
-
-interface SearchInputProps {
-  value: string;
-  onChange: (value: string) => void;
-  onButtonClick: () => void;
-  loading?: boolean;
-  className?: string;
-  buttonText?: Text;
-}
-
-export default function SearchInput({
-  value,
-  onChange,
-  onButtonClick,
-  loading,
-  className,
-  buttonText,
-}: SearchInputProps) {
-  const c = useC();
-
-  return (
-    <div className={classNames("cru-search-input", className)}>
-      <input
-        type="text"
-        className="cru-search-input-input"
-        value={value}
-        onChange={(event) => {
-          const { value } = event.currentTarget;
-          onChange(value);
-        }}
-        onKeyDown={(event) => {
-          if (event.key === "Enter") {
-            onButtonClick();
-            event.preventDefault();
-          }
-        }}
-      />
-
-      <LoadingButton loading={loading} onClick={onButtonClick}>
-        {c(buttonText ?? "search")}
-      </LoadingButton>
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/Skeleton.css b/FrontEnd/src/views/common/Skeleton.css
deleted file mode 100644
index a571eead..00000000
--- a/FrontEnd/src/views/common/Skeleton.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.cru-skeleton {
-  padding: 0 1em;
-}
-
-.cru-skeleton-line {
-  height: 1em;
-  background-color: hsl(0, 0%, 90%);
-  margin: 0.7em 0;
-  border-radius: 0.2em;
-}
-
-.cru-skeleton-line.last {
-  width: 50%;
-}
diff --git a/FrontEnd/src/views/common/Skeleton.tsx b/FrontEnd/src/views/common/Skeleton.tsx
deleted file mode 100644
index 3b149db9..00000000
--- a/FrontEnd/src/views/common/Skeleton.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import range from "lodash/range";
-
-import "./Skeleton.css";
-
-export interface SkeletonProps {
-  lineNumber?: number;
-  className?: string;
-  style?: React.CSSProperties;
-}
-
-const Skeleton: React.FC<SkeletonProps> = (props) => {
-  const { lineNumber: lineNumberProps, className, style } = props;
-  const lineNumber = lineNumberProps ?? 3;
-
-  return (
-    <div className={classnames(className, "cru-skeleton")} style={style}>
-      {range(lineNumber).map((i) => (
-        <div
-          key={i}
-          className={classnames(
-            "cru-skeleton-line",
-            i === lineNumber - 1 && "last"
-          )}
-        />
-      ))}
-    </div>
-  );
-};
-
-export default Skeleton;
diff --git a/FrontEnd/src/views/common/Spinner.css b/FrontEnd/src/views/common/Spinner.css
deleted file mode 100644
index a1de68d2..00000000
--- a/FrontEnd/src/views/common/Spinner.css
+++ /dev/null
@@ -1,13 +0,0 @@
-@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
deleted file mode 100644
index ec0c2c35..00000000
--- a/FrontEnd/src/views/common/Spinner.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { CSSProperties } from "react";
-import classnames from "classnames";
-
-import { ThemeColor } from "./common";
-
-import "./Spinner.css";
-
-export interface SpinnerProps {
-  size?: "sm" | "md" | "lg" | number | string;
-  color?: ThemeColor;
-  className?: string;
-  style?: CSSProperties;
-}
-
-export default function Spinner(props: SpinnerProps) {
-  const { size, color, className, style } = props;
-  const calculatedSize =
-    size === "sm"
-      ? "18px"
-      : size === "md"
-      ? "30px"
-      : size === "lg"
-      ? "42px"
-      : typeof size === "number"
-      ? size
-      : size == null
-      ? "20px"
-      : size;
-
-  return (
-    <span
-      className={classnames("cru-spinner", color && `cru-${color}`, className)}
-      style={{ width: calculatedSize, height: calculatedSize, ...style }}
-    />
-  );
-}
diff --git a/FrontEnd/src/views/common/TimelineLogo.tsx b/FrontEnd/src/views/common/TimelineLogo.tsx
deleted file mode 100644
index e06ed0f5..00000000
--- a/FrontEnd/src/views/common/TimelineLogo.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { SVGAttributes } from "react";
-import * as React from "react";
-
-export interface TimelineLogoProps extends SVGAttributes<SVGElement> {
-  color?: string;
-}
-
-const TimelineLogo: React.FC<TimelineLogoProps> = (props) => {
-  const { color, ...forwardProps } = props;
-  const coercedColor = color ?? "currentcolor";
-  return (
-    <svg
-      className={props.className}
-      viewBox="0 0 100 100"
-      fill="none"
-      strokeWidth="12"
-      stroke={coercedColor}
-      {...forwardProps}
-    >
-      <line x1="50" y1="0" x2="50" y2="25" />
-      <circle cx="50" cy="50" r="22" />
-      <line x1="50" y1="75" x2="50" y2="100" />
-    </svg>
-  );
-};
-
-export default TimelineLogo;
diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx
deleted file mode 100644
index 42074781..00000000
--- a/FrontEnd/src/views/common/alert/AlertHost.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import * as React from "react";
-import without from "lodash/without";
-import { useTranslation } from "react-i18next";
-import classNames from "classnames";
-
-import { alertService, AlertInfoEx, AlertInfo } from "@/services/alert";
-import { convertI18nText } from "@/common";
-
-import IconButton from "../button/IconButton";
-
-import "./alert.css";
-
-interface AutoCloseAlertProps {
-  alert: AlertInfo;
-  close: () => void;
-}
-
-export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
-  const { alert, close } = props;
-  const { dismissTime } = alert;
-
-  const { t } = useTranslation();
-
-  const timerTag = React.useRef<number | null>(null);
-  const closeHandler = React.useRef<(() => void) | null>(null);
-
-  React.useEffect(() => {
-    closeHandler.current = close;
-  }, [close]);
-
-  React.useEffect(() => {
-    const tag =
-      dismissTime === "never"
-        ? null
-        : typeof dismissTime === "number"
-        ? window.setTimeout(() => closeHandler.current?.(), dismissTime)
-        : window.setTimeout(() => closeHandler.current?.(), 5000);
-    timerTag.current = tag;
-    return () => {
-      if (tag != null) {
-        window.clearTimeout(tag);
-      }
-    };
-  }, [dismissTime]);
-
-  const cancelTimer = (): void => {
-    const { current: tag } = timerTag;
-    if (tag != null) {
-      window.clearTimeout(tag);
-    }
-  };
-
-  return (
-    <div
-      className={classNames(
-        "m-3 cru-alert",
-        "cru-" + (alert.type ?? "primary")
-      )}
-      onClick={cancelTimer}
-    >
-      <div className="cru-alert-content">
-        {(() => {
-          const { message, customMessage } = alert;
-          if (customMessage != null) {
-            return customMessage;
-          } else {
-            return convertI18nText(message, t);
-          }
-        })()}
-      </div>
-      <div className="cru-alert-close-button-container">
-        <IconButton
-          icon="x"
-          className="cru-alert-close-button"
-          onClick={close}
-        />
-      </div>
-    </div>
-  );
-};
-
-const AlertHost: React.FC = () => {
-  const [alerts, setAlerts] = React.useState<AlertInfoEx[]>([]);
-
-  React.useEffect(() => {
-    const consume = (alert: AlertInfoEx): void => {
-      setAlerts((old) => [...old, alert]);
-    };
-
-    alertService.registerConsumer(consume);
-    return () => {
-      alertService.unregisterConsumer(consume);
-    };
-  }, []);
-
-  return (
-    <div className="alert-container">
-      {alerts.map((alert) => {
-        return (
-          <AutoCloseAlert
-            key={alert.id}
-            alert={alert}
-            close={() => {
-              setAlerts((old) => without(old, alert));
-            }}
-          />
-        );
-      })}
-    </div>
-  );
-};
-
-export default AlertHost;
diff --git a/FrontEnd/src/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css
deleted file mode 100644
index 54c2b87f..00000000
--- a/FrontEnd/src/views/common/alert/alert.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.alert-container {
-  position: fixed;
-  z-index: 1040;
-}
-
-.cru-alert {
-  border-radius: 5px;
-  border: var(--cru-key-color) 1px solid;
-  color: var(--cru-key-t-color);
-  background-color: var(--cru-key-b1-color);
-
-  display: flex;
-  overflow: hidden;
-}
-
-.cru-alert-content {
-  padding: 0.5em 2em;
-}
-
-.cru-alert-close-button-container {
-  flex-shrink: 0;
-  margin-left: auto;
-  width: 2em;
-  text-align: center;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background-color: var(--cru-key-t-color);
-}
-
-.cru-alert-close-button {
-  color: var(--cru-key-color);
-}
diff --git a/FrontEnd/src/views/common/breakpoints.ts b/FrontEnd/src/views/common/breakpoints.ts
deleted file mode 100644
index fb281610..00000000
--- a/FrontEnd/src/views/common/breakpoints.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const breakpoints = {
-  sm: 576,
-} as const;
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css
deleted file mode 100644
index 1da70f0e..00000000
--- a/FrontEnd/src/views/common/button/Button.css
+++ /dev/null
@@ -1,64 +0,0 @@
-.cru-button {
-  font-size: 1rem;
-  padding: 0.4em 0.8em;
-  transition: all 0.3s;
-  border-radius: 0.2em;
-  border: 1px solid;
-  cursor: pointer;
-}
-
-.cru-button:not(.outline) {
-  color: var(--cru-push-button-text-color);
-  background-color: var(--cru-clickable-normal-color);
-  border-color: var(--cru-clickable-normal-color);
-}
-
-.cru-button:not(.outline):hover {
-  background-color: var(--cru-clickable-hover-color);
-  border-color: var(--cru-clickable-hover-color);
-}
-
-.cru-button:not(.outline):focus {
-  background-color: var(--cru-clickable-focus-color);
-  border-color: var(--cru-clickable-focus-color);
-}
-
-.cru-button:not(.outline):active {
-  background-color: var(--cru-clickable-active-color);
-  border-color: var(--cru-clickable-active-color);
-}
-
-.cru-button:not(.outline):disabled {
-  color: var(--cru-push-button-disabled-text-color);
-  background-color: var(--cru-push-button-disabled-color);
-  border-color: var(--cru-push-button-disabled-color);
-  cursor: auto;
-}
-
-
-.cru-button.outline {
-  color: var(--cru-clickable-normal-color);
-  border-color: var(--cru-clickable-normal-color);
-  background-color: transparent;
-}
-
-.cru-button.outline:hover {
-  color: var(--cru-clickable-hover-color);
-  border-color: var(--cru-clickable-hover-color);
-}
-
-.cru-button.outline:focus {
-  color: var(--cru-clickable-focus-color);
-  border-color: var(--cru-clickable-focus-color);
-}
-
-.cru-button.outline:active {
-  color: var(--cru-clickable-active-color);
-  border-color: var(--cru-clickable-active-color);
-}
-
-.cru-button.outline:disabled {
-  color: var(--cru-clickable-disabled-color);
-  border-color: var(--cru-clickable-disabled-color);
-  cursor: auto;
-}
diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx
deleted file mode 100644
index 6c38e130..00000000
--- a/FrontEnd/src/views/common/button/Button.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import { Text, useC, ThemeColor } from "../common";
-
-import "./Button.css";
-
-interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
-  color?: ThemeColor;
-  text?: Text;
-  outline?: boolean;
-  buttonRef?: Ref<HTMLButtonElement> | null;
-}
-
-export default function Button(props: ButtonProps) {
-  const {
-    buttonRef,
-    color,
-    text,
-    outline,
-    className,
-    children,
-    ...otherProps
-  } = props;
-
-  if (text != null && children != null) {
-    console.warn("You can't set both text and children props.");
-  }
-
-  const c = useC();
-
-  return (
-    <button
-      ref={buttonRef}
-      className={classNames(
-        "cru-button",
-        `cru-clickable-${color ?? "primary"}`,
-        outline && "outline",
-        className,
-      )}
-      {...otherProps}
-    >
-      {text != null ? c(text) : children}
-    </button>
-  );
-}
diff --git a/FrontEnd/src/views/common/button/ButtonRow.css b/FrontEnd/src/views/common/button/ButtonRow.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/FrontEnd/src/views/common/button/ButtonRow.tsx b/FrontEnd/src/views/common/button/ButtonRow.tsx
deleted file mode 100644
index eea60cc4..00000000
--- a/FrontEnd/src/views/common/button/ButtonRow.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import Button from "./Button";
-import FlatButton from "./FlatButton";
-import IconButton from "./IconButton";
-import LoadingButton from "./LoadingButton";
-
-import "./ButtonRow.css";
-
-type ButtonRowButton = (
-  | {
-      type: "normal";
-      props: ComponentPropsWithoutRef<typeof Button>;
-    }
-  | {
-      type: "flat";
-      props: ComponentPropsWithoutRef<typeof FlatButton>;
-    }
-  | {
-      type: "icon";
-      props: ComponentPropsWithoutRef<typeof IconButton>;
-    }
-  | { type: "loading"; props: ComponentPropsWithoutRef<typeof LoadingButton> }
-) & { key: string | number };
-
-interface ButtonRowProps {
-  className?: string;
-  containerRef?: Ref<HTMLDivElement>;
-  buttons: ButtonRowButton[];
-  buttonsClassName?: string;
-}
-
-export default function ButtonRow({
-  className,
-  containerRef,
-  buttons,
-  buttonsClassName,
-}: ButtonRowProps) {
-  return (
-    <div ref={containerRef} className={classNames("cru-button-row", className)}>
-      {buttons.map((button) => {
-        const { type, key, props } = button;
-        const newClassName = classNames(props.className, buttonsClassName);
-        switch (type) {
-          case "normal":
-            return <Button key={key} {...props} className={newClassName} />;
-          case "flat":
-            return <FlatButton key={key} {...props} className={newClassName} />;
-          case "icon":
-            return <IconButton key={key} {...props} className={newClassName} />;
-          case "loading":
-            return (
-              <LoadingButton key={key} {...props} className={newClassName} />
-            );
-          default:
-            throw new Error();
-        }
-      })}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/button/ButtonRowV2.tsx b/FrontEnd/src/views/common/button/ButtonRowV2.tsx
deleted file mode 100644
index 3467ad52..00000000
--- a/FrontEnd/src/views/common/button/ButtonRowV2.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import Button from "./Button";
-import FlatButton from "./FlatButton";
-import IconButton from "./IconButton";
-import LoadingButton from "./LoadingButton";
-
-import "./ButtonRow.css";
-import { Text, ThemeColor } from "../common";
-
-interface ButtonRowV2ButtonBase {
-  key: string | number;
-  action?: "primary" | "secondary";
-  color?: ThemeColor;
-  disabled?: boolean;
-  onClick?: () => void;
-}
-
-interface ButtonRowV2ButtonWithNoType extends ButtonRowV2ButtonBase {
-  type?: undefined | null;
-  text: Text;
-  outline?: boolean;
-  props?: ComponentPropsWithoutRef<typeof Button>;
-}
-
-interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase {
-  type: "normal";
-  text: Text;
-  outline?: boolean;
-  props?: ComponentPropsWithoutRef<typeof Button>;
-}
-
-interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase {
-  type: "flat";
-  text: Text;
-  props?: ComponentPropsWithoutRef<typeof FlatButton>;
-}
-
-interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase {
-  type: "icon";
-  icon: string;
-  props?: ComponentPropsWithoutRef<typeof IconButton>;
-}
-
-interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase {
-  type: "loading";
-  text: Text;
-  loading?: boolean;
-  props?: ComponentPropsWithoutRef<typeof LoadingButton>;
-}
-
-type ButtonRowV2Button =
-  | ButtonRowV2ButtonWithNoType
-  | ButtonRowV2NormalButton
-  | ButtonRowV2FlatButton
-  | ButtonRowV2IconButton
-  | ButtonRowV2LoadingButton;
-
-interface ButtonRowV2Props {
-  className?: string;
-  containerRef?: Ref<HTMLDivElement>;
-  buttons: ButtonRowV2Button[];
-  buttonsClassName?: string;
-}
-
-export default function ButtonRowV2({
-  className,
-  containerRef,
-  buttons,
-  buttonsClassName,
-}: ButtonRowV2Props) {
-  return (
-    <div ref={containerRef} className={classNames("cru-button-row", className)}>
-      {buttons.map((button) => {
-        const { key, action, color, disabled, onClick } = button;
-
-        const realAction = action ?? "primary";
-        const realColor =
-          color ?? (realAction === "primary" ? "primary" : "secondary");
-
-        const commonProps = { key, color: realColor, disabled, onClick };
-        const newClassName = classNames(
-          button.props?.className,
-          buttonsClassName,
-        );
-
-        switch (button.type) {
-          case null:
-          case undefined:
-          case "normal": {
-            const { text, outline, props } = button;
-            return (
-              <Button
-                {...commonProps}
-                text={text}
-                outline={outline ?? realAction !== "primary"}
-                {...props}
-                className={newClassName}
-              />
-            );
-          }
-          case "flat": {
-            const { text, props } = button;
-            return (
-              <FlatButton
-                {...commonProps}
-                text={text}
-                {...props}
-                className={newClassName}
-              />
-            );
-          }
-          case "icon": {
-            const { icon, props } = button;
-            return (
-              <IconButton
-                {...commonProps}
-                icon={icon}
-                {...props}
-                className={newClassName}
-              />
-            );
-          }
-          case "loading": {
-            const { text, loading, props } = button;
-            return (
-              <LoadingButton
-                {...commonProps}
-                text={text}
-                loading={loading}
-                {...props}
-                className={newClassName}
-              />
-            );
-          }
-          default:
-            throw new Error();
-        }
-      })}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css
deleted file mode 100644
index 2050946c..00000000
--- a/FrontEnd/src/views/common/button/FlatButton.css
+++ /dev/null
@@ -1,27 +0,0 @@
-.cru-flat-button {
-  font-size: 1rem;
-  padding: 0.4em 0.8em;
-  transition: all 0.5s;
-  border-radius: 0.2em;
-  background-color: var(--cru-clickable-grayscale-normal-color);
-  border: 1px none;
-  color: var(--cru-clickable-normal-color);
-  cursor: pointer;
-}
-
-.cru-flat-button:hover {
-  background-color: var(--cru-clickable-grayscale-hover-color);
-}
-
-.cru-flat-button:focus {
-  background-color: var(--cru-clickable-grayscale-focus-color);
-}
-
-.cru-flat-button:active {
-  background-color: var(--cru-clickable-grayscale-active-color);
-}
-
-.cru-flat-button:disabled {
-  color: var(--cru-clickable-disabled-color);
-  cursor: auto;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx
deleted file mode 100644
index 9f074dd6..00000000
--- a/FrontEnd/src/views/common/button/FlatButton.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import { Text, useC, ThemeColor } from "../common";
-
-import "./FlatButton.css";
-
-interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> {
-  color?: ThemeColor;
-  text?: Text;
-  buttonRef?: Ref<HTMLButtonElement> | null;
-}
-
-export default function FlatButton(props: FlatButtonProps) {
-  const { color, text, className, children, buttonRef, ...otherProps } = props;
-
-  if (text != null && children != null) {
-    console.warn("You can't set both text and children props.");
-  }
-
-  const c = useC();
-
-  return (
-    <button
-      ref={buttonRef}
-      className={classNames(
-        "cru-flat-button",
-        `cru-clickable-${color ?? "primary"}`,
-        className,
-      )}
-      {...otherProps}
-    >
-      {text != null ? c(text) : children}
-    </button>
-  );
-}
diff --git a/FrontEnd/src/views/common/button/IconButton.css b/FrontEnd/src/views/common/button/IconButton.css
deleted file mode 100644
index a3747201..00000000
--- a/FrontEnd/src/views/common/button/IconButton.css
+++ /dev/null
@@ -1,30 +0,0 @@
-.cru-icon-button {
-  color: var(--cru-clickable-normal-color);
-  font-size: 1.4rem;
-  background: none;
-  border: none;
-  transition: all 0.5s;
-  cursor: pointer;
-  user-select: none;
-}
-
-.cru-icon-button:hover {
-  color: var(--cru-clickable-hover-color);
-}
-
-.cru-icon-button:focus {
-  color: var(--cru-clickable-focus-color);
-}
-
-.cru-icon-button:active {
-  color: var(--cru-clickable-active-color);
-}
-
-.cru-flat-button:disabled {
-  color: var(--cru-clickable-disabled-color);
-  cursor: auto;
-}
-
-.cru-icon-button.large {
-  font-size: 1.6rem;
-}
diff --git a/FrontEnd/src/views/common/button/IconButton.tsx b/FrontEnd/src/views/common/button/IconButton.tsx
deleted file mode 100644
index 95c58887..00000000
--- a/FrontEnd/src/views/common/button/IconButton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ComponentPropsWithoutRef } from "react";
-import classNames from "classnames";
-
-import { ThemeColor } from "../common";
-
-import "./IconButton.css";
-
-interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
-  icon: string;
-  color?: ThemeColor | "grayscale";
-  large?: boolean;
-  disabled?: boolean; // TODO: Not implemented
-}
-
-export default function IconButton(props: IconButtonProps) {
-  const { icon, color, className, large, ...otherProps } = props;
-
-  return (
-    <button
-      className={classNames(
-        "cru-icon-button",
-        `cru-clickable-${color ?? "grayscale"}`,
-        large && "large",
-        "bi-" + icon,
-        className,
-      )}
-      {...otherProps}
-    />
-  );
-}
diff --git a/FrontEnd/src/views/common/button/LoadingButton.css b/FrontEnd/src/views/common/button/LoadingButton.css
deleted file mode 100644
index 23fadd3d..00000000
--- a/FrontEnd/src/views/common/button/LoadingButton.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.cru-loading-button {
-  display: flex;
-  align-items: center;
-}
-
-.cru-loading-button-spinner {
-  margin-left: 0.5em;
-}
-
-.cru-loading-button-loading {
-  color: var(--cru-clickable-normal-color) !important;
-  border-color: var(--cru-clickable-normal-color) !important;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
deleted file mode 100644
index 7e7d08e6..00000000
--- a/FrontEnd/src/views/common/button/LoadingButton.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import classNames from "classnames";
-
-import { I18nText, ThemeColor, useC } from "../common";
-
-import Spinner from "../Spinner";
-
-import "./LoadingButton.css";
-
-interface LoadingButtonProps extends React.ComponentPropsWithoutRef<"button"> {
-  color?: ThemeColor;
-  text?: I18nText;
-  loading?: boolean;
-}
-
-export default function LoadingButton(props: LoadingButtonProps) {
-  const c = useC();
-
-  const { color, text, loading, disabled, className, children, ...otherProps } =
-    props;
-
-  if (text != null && children != null) {
-    console.warn("You can't set both text and children props.");
-  }
-
-  return (
-    <button
-      disabled={disabled || loading}
-      className={classNames(
-        "cru-button outline cru-loading-button",
-        `cru-clickable-${color ?? "primary"}`,
-        loading && "cru-loading-button-loading",
-        className,
-      )}
-      {...otherProps}
-    >
-      {text != null ? c(text) : children}
-      {loading && <Spinner className="cru-loading-button-spinner" />}
-    </button>
-  );
-}
diff --git a/FrontEnd/src/views/common/button/index.tsx b/FrontEnd/src/views/common/button/index.tsx
deleted file mode 100644
index b5aa5470..00000000
--- a/FrontEnd/src/views/common/button/index.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import Button from "./Button";
-import FlatButton from "./FlatButton";
-import IconButton from "./IconButton";
-import LoadingButton from "./LoadingButton";
-import ButtonRow from "./ButtonRow";
-import ButtonRowV2 from "./ButtonRowV2";
-
-export {
-  Button,
-  FlatButton,
-  IconButton,
-  LoadingButton,
-  ButtonRow,
-  ButtonRowV2,
-};
diff --git a/FrontEnd/src/views/common/common.ts b/FrontEnd/src/views/common/common.ts
deleted file mode 100644
index 7af2643b..00000000
--- a/FrontEnd/src/views/common/common.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export type { Text, I18nText } from "@/common";
-export { c, convertI18nText, useC } from "@/common";
-
-export const themeColors = [
-  "primary",
-  "secondary",
-  "danger",
-  "create",
-] as const;
-
-export type ThemeColor = (typeof themeColors)[number];
-
-export { breakpoints } from "./breakpoints";
-export { useMobile } from "./hooks";
diff --git a/FrontEnd/src/views/common/dialog/ConfirmDialog.css b/FrontEnd/src/views/common/dialog/ConfirmDialog.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
deleted file mode 100644
index dbbd15c6..00000000
--- a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useC, Text, ThemeColor } from "@/views/common/common";
-
-import Dialog from "./Dialog";
-import DialogContainer from "./DialogContainer";
-
-export default function ConfirmDialog({
-  open,
-  onClose,
-  onConfirm,
-  title,
-  body,
-  color,
-  bodyColor,
-}: {
-  open: boolean;
-  onClose: () => void;
-  onConfirm: () => void;
-  title: Text;
-  body: Text;
-  color?: ThemeColor;
-  bodyColor?: ThemeColor;
-}) {
-  const c = useC();
-
-  return (
-    <Dialog onClose={onClose} open={open}>
-      <DialogContainer
-        title={title}
-        titleColor={color ?? "danger"}
-        buttons={[
-          {
-            key: "cancel",
-            type: "normal",
-            props: {
-              text: "operationDialog.cancel",
-              color: "secondary",
-              outline: true,
-              onClick: onClose,
-            },
-          },
-          {
-            key: "confirm",
-            type: "normal",
-            props: {
-              text: "operationDialog.confirm",
-              color: "danger",
-              onClick: () => {
-                onConfirm();
-                onClose();
-              },
-            },
-          },
-        ]}
-      >
-        <div className={`cru-${bodyColor ?? "primary"}`}>{c(body)}</div>
-      </DialogContainer>
-    </Dialog>
-  );
-}
diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css
deleted file mode 100644
index e4c61440..00000000
--- a/FrontEnd/src/views/common/dialog/Dialog.css
+++ /dev/null
@@ -1,60 +0,0 @@
-.cru-dialog-overlay {
-  position: fixed;
-  z-index: 1040;
-  left: 0;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  display: flex;
-  align-items: center;
-  overflow: auto;
-}
-
-.cru-dialog-background {
-  position: absolute;
-  z-index: -1;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  background-color: var(--cru-surface-dim-color);
-  opacity: 0.8;
-}
-
-.cru-dialog-container {
-  max-width: 100%;
-  min-width: 30vw;
-
-  margin: 2em auto;
-
-  border: var(--cru-key-container-color) 1px solid;
-  border-radius: 5px;
-  padding: 1.5em;
-  background-color: var(--cru-surface-color);
-}
-
-@media (min-width: 576px) {
-  .cru-dialog-container {
-    max-width: 800px;
-  }
-}
-
-.cru-dialog-enter .cru-dialog-container {
-  transform: scale(0, 0);
-  opacity: 0;
-  transform-origin: center;
-}
-
-.cru-dialog-enter-active .cru-dialog-container {
-  transform: scale(1, 1);
-  opacity: 1;
-  transition: transform 0.3s, opacity 0.3s;
-  transform-origin: center;
-}
-
-.cru-dialog-exit-active .cru-dialog-container {
-  transition: transform 0.3s, opacity 0.3s;
-  transform: scale(0, 0);
-  opacity: 0;
-  transform-origin: center;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx
deleted file mode 100644
index 2ff7bea8..00000000
--- a/FrontEnd/src/views/common/dialog/Dialog.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { ReactNode, useRef } from "react";
-import ReactDOM from "react-dom";
-import { CSSTransition } from "react-transition-group";
-import classNames from "classnames";
-
-import { ThemeColor } from "../common";
-
-import "./Dialog.css";
-
-const optionalPortalElement = document.getElementById("portal");
-if (optionalPortalElement == null) {
-  throw new Error("Portal element not found");
-}
-const portalElement = optionalPortalElement;
-
-interface DialogProps {
-  open: boolean;
-  onClose: () => void;
-  color?: ThemeColor;
-  children?: ReactNode;
-  disableCloseOnClickOnOverlay?: boolean;
-}
-
-export default function Dialog({
-  open,
-  onClose,
-  color,
-  children,
-  disableCloseOnClickOnOverlay,
-}: DialogProps) {
-  color = color ?? "primary";
-
-  const nodeRef = useRef(null);
-
-  return ReactDOM.createPortal(
-    <CSSTransition
-      nodeRef={nodeRef}
-      mountOnEnter
-      unmountOnExit
-      in={open}
-      timeout={300}
-      classNames="cru-dialog"
-    >
-      <div
-        ref={nodeRef}
-        className={classNames("cru-dialog-overlay", `cru-${color}`)}
-      >
-        <div
-          className="cru-dialog-background"
-          onClick={
-            disableCloseOnClickOnOverlay
-              ? undefined
-              : () => {
-                  onClose();
-                }
-          }
-        />
-        <div className="cru-dialog-container">{children}</div>
-      </div>
-    </CSSTransition>,
-    portalElement,
-  );
-}
diff --git a/FrontEnd/src/views/common/dialog/DialogContainer.css b/FrontEnd/src/views/common/dialog/DialogContainer.css
deleted file mode 100644
index fbb18e0d..00000000
--- a/FrontEnd/src/views/common/dialog/DialogContainer.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.cru-dialog-container-title {
-  font-size: 1.2em;
-  font-weight: bold;
-  color: var(--cru-key-color);
-  margin-bottom: 0.5em;
-}
-
-
-.cru-dialog-container-hr {
-  margin: 1em 0;
-}
-
-.cru-dialog-container-button-row {
-  display: flex;
-  justify-content: flex-end;
-}
-
-.cru-dialog-container-button {
-  margin-left: 1em;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/dialog/DialogContainer.tsx b/FrontEnd/src/views/common/dialog/DialogContainer.tsx
deleted file mode 100644
index afee2669..00000000
--- a/FrontEnd/src/views/common/dialog/DialogContainer.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { ComponentProps, Ref, ReactNode } from "react";
-import classNames from "classnames";
-
-import { ThemeColor, Text, useC } from "../common";
-import { ButtonRow, ButtonRowV2 } from "../button";
-
-import "./DialogContainer.css";
-
-interface DialogContainerBaseProps {
-  className?: string;
-  title: Text;
-  titleColor?: ThemeColor;
-  titleClassName?: string;
-  titleRef?: Ref<HTMLDivElement>;
-  bodyContainerClassName?: string;
-  bodyContainerRef?: Ref<HTMLDivElement>;
-  buttonsClassName?: string;
-  buttonsContainerRef?: ComponentProps<typeof ButtonRow>["containerRef"];
-  children: ReactNode;
-}
-
-interface DialogContainerWithButtonsProps extends DialogContainerBaseProps {
-  buttons: ComponentProps<typeof ButtonRow>["buttons"];
-}
-
-interface DialogContainerWithButtonsV2Props extends DialogContainerBaseProps {
-  buttonsV2: ComponentProps<typeof ButtonRowV2>["buttons"];
-}
-
-type DialogContainerProps =
-  | DialogContainerWithButtonsProps
-  | DialogContainerWithButtonsV2Props;
-
-export default function DialogContainer(props: DialogContainerProps) {
-  const {
-    className,
-    title,
-    titleColor,
-    titleClassName,
-    titleRef,
-    bodyContainerClassName,
-    bodyContainerRef,
-    buttonsClassName,
-    buttonsContainerRef,
-    children,
-  } = props;
-
-  const c = useC();
-
-  return (
-    <div className={classNames(className)}>
-      <div
-        ref={titleRef}
-        className={classNames(
-          `cru-dialog-container-title cru-${titleColor ?? "primary"}`,
-          titleClassName,
-        )}
-      >
-        {c(title)}
-      </div>
-      <hr className="cru-dialog-container-hr" />
-      <div
-        ref={bodyContainerRef}
-        className={classNames(
-          "cru-dialog-container-body",
-          bodyContainerClassName,
-        )}
-      >
-        {children}
-      </div>
-      <hr className="cru-dialog-container-hr" />
-      {"buttons" in props ? (
-        <ButtonRow
-          containerRef={buttonsContainerRef}
-          className={classNames(
-            "cru-dialog-container-button-row",
-            buttonsClassName,
-          )}
-          buttons={props.buttons}
-          buttonsClassName="cru-dialog-container-button"
-        />
-      ) : (
-        <ButtonRowV2
-          containerRef={buttonsContainerRef}
-          className={classNames(
-            "cru-dialog-container-button-row",
-            buttonsClassName,
-          )}
-          buttons={props.buttonsV2}
-          buttonsClassName="cru-dialog-container-button"
-        />
-      )}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.css b/FrontEnd/src/views/common/dialog/FullPageDialog.css
deleted file mode 100644
index 2f1fc636..00000000
--- a/FrontEnd/src/views/common/dialog/FullPageDialog.css
+++ /dev/null
@@ -1,44 +0,0 @@
-.cru-full-page {
-  position: fixed;
-  z-index: 1030;
-  left: 0;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  background-color: white;
-  padding-top: 56px;
-}
-
-.cru-full-page-top-bar {
-  height: 56px;
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  z-index: 1;
-  background-color: var(--cru-primary-color);
-  display: flex;
-  align-items: center;
-}
-
-.cru-full-page-content-container {
-  overflow: scroll;
-}
-
-.cru-full-page-back-button {
-  color: var(--cru-primary-t-color);
-}
-
-.cru-full-page-enter {
-  transform: translate(100%, 0);
-}
-
-.cru-full-page-enter-active {
-  transform: none;
-  transition: transform 0.3s;
-}
-
-.cru-full-page-exit-active {
-  transition: transform 0.3s;
-  transform: translate(100%, 0);
-}
diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx b/FrontEnd/src/views/common/dialog/FullPageDialog.tsx
deleted file mode 100644
index 6368fc0a..00000000
--- a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as React from "react";
-import { createPortal } from "react-dom";
-import classnames from "classnames";
-import { CSSTransition } from "react-transition-group";
-
-import "./FullPageDialog.css";
-import IconButton from "../button/IconButton";
-
-export interface FullPageDialogProps {
-  show: boolean;
-  onBack: () => void;
-  contentContainerClassName?: string;
-  children: React.ReactNode;
-}
-
-const FullPageDialog: React.FC<FullPageDialogProps> = ({
-  show,
-  onBack,
-  children,
-  contentContainerClassName,
-}) => {
-  return createPortal(
-    <CSSTransition
-      mountOnEnter
-      unmountOnExit
-      in={show}
-      timeout={300}
-      classNames="cru-full-page"
-    >
-      <div className="cru-full-page">
-        <div className="cru-full-page-top-bar">
-          <IconButton
-            icon="arrow-left"
-            className="ms-3 cru-full-page-back-button"
-            onClick={onBack}
-          />
-        </div>
-        <div
-          className={classnames(
-            "cru-full-page-content-container",
-            contentContainerClassName
-          )}
-        >
-          {children}
-        </div>
-      </div>
-    </CSSTransition>,
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    document.getElementById("portal")!
-  );
-};
-
-export default FullPageDialog;
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css
deleted file mode 100644
index f4b7237e..00000000
--- a/FrontEnd/src/views/common/dialog/OperationDialog.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.cru-operation-dialog-prompt {
-  color: var(--cru-surface-on-color);
-}
-
-.cru-operation-dialog-input-group {
-  display: block;
-  margin: 0.5em 0;
-}
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
deleted file mode 100644
index 4335b2b0..00000000
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import { useState, ReactNode, ComponentProps } from "react";
-import classNames from "classnames";
-
-import { useC, Text, ThemeColor } from "../common";
-
-import {
-  useInputs,
-  InputGroup,
-  Initializer as InputInitializer,
-  InputValueDict,
-  InputErrorDict,
-} from "../input/InputGroup";
-import Dialog from "./Dialog";
-import DialogContainer from "./DialogContainer";
-
-import "./OperationDialog.css";
-
-export type { InputInitializer, InputValueDict, InputErrorDict };
-
-interface OperationDialogPromptProps {
-  message?: Text;
-  customMessage?: Text;
-  customMessageNode?: ReactNode;
-  className?: string;
-}
-
-function OperationDialogPrompt(props: OperationDialogPromptProps) {
-  const { message, customMessage, customMessageNode, className } = props;
-
-  const c = useC();
-
-  return (
-    <div className={classNames(className, "cru-operation-dialog-prompt")}>
-      {message && <p>{c(message)}</p>}
-      {customMessageNode ?? (customMessage != null ? c(customMessage) : null)}
-    </div>
-  );
-}
-
-export interface OperationDialogProps<TData> {
-  open: boolean;
-  onClose: () => void;
-
-  color?: ThemeColor;
-  inputColor?: ThemeColor;
-  title: Text;
-  inputPrompt?: Text;
-  inputPromptNode?: ReactNode;
-  successPrompt?: (data: TData) => Text;
-  successPromptNode?: (data: TData) => ReactNode;
-  failurePrompt?: (error: unknown) => Text;
-  failurePromptNode?: (error: unknown) => ReactNode;
-
-  inputs: InputInitializer;
-
-  onProcess: (inputs: InputValueDict) => Promise<TData>;
-  onSuccessAndClose?: (data: TData) => void;
-}
-
-function OperationDialog<TData>(props: OperationDialogProps<TData>) {
-  const {
-    open,
-    onClose,
-    color,
-    inputColor,
-    title,
-    inputPrompt,
-    inputPromptNode,
-    successPrompt,
-    successPromptNode,
-    failurePrompt,
-    failurePromptNode,
-    inputs,
-    onProcess,
-    onSuccessAndClose,
-  } = props;
-
-  if (process.env.NODE_ENV === "development") {
-    if (inputPrompt && inputPromptNode) {
-      console.log("InputPrompt and inputPromptNode are both set.");
-    }
-    if (successPrompt && successPromptNode) {
-      console.log("SuccessPrompt and successPromptNode are both set.");
-    }
-    if (failurePrompt && failurePromptNode) {
-      console.log("FailurePrompt and failurePromptNode are both set.");
-    }
-  }
-
-  type Step =
-    | { type: "input" }
-    | { type: "process" }
-    | {
-        type: "success";
-        data: TData;
-      }
-    | {
-        type: "failure";
-        data: unknown;
-      };
-
-  const [step, setStep] = useState<Step>({ type: "input" });
-
-  const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } =
-    useInputs({
-      init: inputs,
-    });
-
-  function close() {
-    if (step.type !== "process") {
-      onClose();
-      if (step.type === "success" && onSuccessAndClose) {
-        onSuccessAndClose?.(step.data);
-      }
-    } else {
-      console.log("Attempt to close modal dialog when processing.");
-    }
-  }
-
-  function onConfirm() {
-    const result = confirm();
-    if (result.type === "ok") {
-      setStep({ type: "process" });
-      setAllDisabled(true);
-      onProcess(result.values).then(
-        (d) => {
-          setStep({
-            type: "success",
-            data: d,
-          });
-        },
-        (e: unknown) => {
-          setStep({
-            type: "failure",
-            data: e,
-          });
-        },
-      );
-    }
-  }
-
-  let body: ReactNode;
-  let buttons: ComponentProps<typeof DialogContainer>["buttons"];
-
-  if (step.type === "input" || step.type === "process") {
-    const isProcessing = step.type === "process";
-
-    body = (
-      <div>
-        <OperationDialogPrompt
-          customMessage={inputPrompt}
-          customMessageNode={inputPromptNode}
-        />
-        <InputGroup
-          containerClassName="cru-operation-dialog-input-group"
-          color={inputColor ?? "primary"}
-          {...inputGroupProps}
-        />
-      </div>
-    );
-    buttons = [
-      {
-        key: "cancel",
-        type: "normal",
-        props: {
-          text: "operationDialog.cancel",
-          color: "secondary",
-          outline: true,
-          onClick: close,
-          disabled: isProcessing,
-        },
-      },
-      {
-        key: "confirm",
-        type: "loading",
-        props: {
-          text: "operationDialog.confirm",
-          color,
-          loading: isProcessing,
-          disabled: hasErrorAndDirty,
-          onClick: onConfirm,
-        },
-      },
-    ];
-  } else {
-    const result = step;
-
-    const promptProps: OperationDialogPromptProps =
-      result.type === "success"
-        ? {
-            message: "operationDialog.success",
-            customMessage: successPrompt?.(result.data),
-            customMessageNode: successPromptNode?.(result.data),
-          }
-        : {
-            message: "operationDialog.error",
-            customMessage: failurePrompt?.(result.data),
-            customMessageNode: failurePromptNode?.(result.data),
-          };
-    body = (
-      <div>
-        <OperationDialogPrompt {...promptProps} />
-      </div>
-    );
-
-    buttons = [
-      {
-        key: "ok",
-        type: "normal",
-        props: {
-          text: "operationDialog.ok",
-          color: "primary",
-          onClick: close,
-        },
-      },
-    ];
-  }
-
-  return (
-    <Dialog open={open} onClose={close}>
-      <DialogContainer title={title} titleColor={color} buttons={buttons}>
-        {body}
-      </DialogContainer>
-    </Dialog>
-  );
-}
-
-export default OperationDialog;
diff --git a/FrontEnd/src/views/common/dialog/index.ts b/FrontEnd/src/views/common/dialog/index.ts
deleted file mode 100644
index 59f15791..00000000
--- a/FrontEnd/src/views/common/dialog/index.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { useState } from "react";
-
-export { default as Dialog } from "./Dialog";
-export { default as FullPageDialog } from "./FullPageDialog";
-export { default as OperationDialog } from "./OperationDialog";
-export { default as ConfirmDialog } from "./ConfirmDialog";
-
-type DialogMap<D extends string, V> = {
-  [K in D]: V;
-};
-
-type DialogKeyMap<D extends string> = DialogMap<D, number>;
-
-type DialogPropsMap<D extends string> = DialogMap<
-  D,
-  { key: number | string; open: boolean; onClose: () => void }
->;
-
-export function useDialog<D extends string>(
-  dialogs: D[],
-  options?: {
-    initDialog?: D | null;
-    onClose?: {
-      [K in D]?: () => void;
-    };
-  },
-): {
-  dialog: D | null;
-  switchDialog: (newDialog: D | null) => void;
-  dialogPropsMap: DialogPropsMap<D>;
-  createDialogSwitch: (newDialog: D | null) => () => void;
-} {
-  const [dialog, setDialog] = useState<D | null>(options?.initDialog ?? null);
-
-  const [dialogKeys, setDialogKeys] = useState<DialogKeyMap<D>>(
-    () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap<D>,
-  );
-
-  const switchDialog = (newDialog: D | null) => {
-    if (dialog !== null) {
-      setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 });
-    }
-    setDialog(newDialog);
-  };
-
-  return {
-    dialog,
-    switchDialog,
-    dialogPropsMap: Object.fromEntries(
-      dialogs.map((d) => [
-        d,
-        {
-          key: `${d}-${dialogKeys[d]}`,
-          open: dialog === d,
-          onClose: () => {
-            switchDialog(null);
-            options?.onClose?.[d]?.();
-          },
-        },
-      ]),
-    ) as DialogPropsMap<D>,
-    createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog),
-  };
-}
diff --git a/FrontEnd/src/views/common/hooks.ts b/FrontEnd/src/views/common/hooks.ts
deleted file mode 100644
index c1fa5774..00000000
--- a/FrontEnd/src/views/common/hooks.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-// TODO: Migrate hooks
-
-export {
-  useIsSmallScreen,
-  useClickOutside,
-  useScrollToBottom,
-} from "@/utilities/hooks";
-
-import { useMediaQuery } from "react-responsive";
-import { breakpoints } from "./breakpoints";
-
-export function useMobile(): boolean {
-  return useMediaQuery({ maxWidth: breakpoints.sm });
-}
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
deleted file mode 100644
index a8f5e9a5..00000000
--- a/FrontEnd/src/views/common/index.css
+++ /dev/null
@@ -1,100 +0,0 @@
-@import "./theme.css";
-
-* {
-  box-sizing: border-box;
-  margin-inline: 0;
-  margin-block: 0;
-}
-
-body {
-  font-family: var(--cru-default-font-family);
-  background: var(--cru-body-background-color);
-  color: var(--cru-text-primary-color);
-  line-height: 1.2;
-}
-
-.cru-page {
-  padding: var(--cru-page-padding);
-}
-
-.cru-page-no-top-padding {
-  padding-top: 0;
-}
-
-.cru-text-center {
-  text-align: center;
-}
-
-.cru-text-end {
-  text-align: end;
-}
-
-.cru-float-left {
-  float: left;
-}
-
-.cru-float-right {
-  float: right;
-}
-
-.cru-align-text-bottom {
-  vertical-align: text-bottom;
-}
-
-.cru-align-middle {
-  vertical-align: middle;
-}
-
-.cru-clearfix::after {
-  clear: both;
-}
-
-.cru-fill-parent {
-  width: 100%;
-  height: 100%;
-}
-
-.cru-avatar {
-  width: 60px;
-  height: 60px;
-}
-
-.cru-avatar.large {
-  width: 100px;
-  height: 100px;
-}
-
-.cru-avatar.small {
-  width: 40px;
-  height: 40px;
-}
-
-.cru-round {
-  border-radius: 50%;
-}
-
-.cru-tab-pages-action-area {
-  display: flex;
-  align-items: center;
-}
-
-.alert-container {
-  position: fixed;
-  z-index: 1070;
-}
-
-@media (min-width: 576px) {
-  .alert-container {
-    bottom: 0;
-    right: 0;
-  }
-}
-
-@media (max-width: 575.98px) {
-  .alert-container {
-    bottom: 0;
-    right: 0;
-    left: 0;
-    text-align: center;
-  }
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/input/InputGroup.css b/FrontEnd/src/views/common/input/InputGroup.css
deleted file mode 100644
index 7e905b1e..00000000
--- a/FrontEnd/src/views/common/input/InputGroup.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.cru-input-group {
-  display: block;
-}
-
-.cru-input-container {
-  margin: 0.4em 0;
-}
-
-.cru-input-label {
-  display: block;
-  color: var(--cru-clickable-normal-color);
-  font-size: 0.9em;
-  margin-bottom: 0.3em;
-}
-
-.cru-input-label-inline {
-  margin-inline-start: 0.5em;
-}
-
-.cru-input-type-text input {
-  appearance: none;
-  display: block;
-  border: 1px solid;
-  /* color: var(--cru-surface-on-color); */
-  /* background-color: var(--cru-surface-color); */
-  margin: 0;
-  font-size: 1em;
-  padding: 0.2em;
-}
-
-.cru-input-type-text input:hover {
-  border-color: var(--cru-clickable-hover-color);
-}
-
-.cru-input-type-text input:focus {
-  border-color: var(--cru-clickable-focus-color);
-}
-
-.cru-input-type-text input:disabled {
-  border-color: var(--cru-clickable-disabled-color);
-}
-
-.cru-input-error {
-  display: block;
-  font-size: 0.8em;
-  color: var(--cru-danger-color);
-  margin-top: 0.4em;
-}
-
-.cru-input-helper {
-  display: block;
-  font-size: 0.8em;
-  color: var(--cru-primary-color);
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx
deleted file mode 100644
index d95bb29e..00000000
--- a/FrontEnd/src/views/common/input/InputGroup.tsx
+++ /dev/null
@@ -1,461 +0,0 @@
-/**
- * Some notes for InputGroup:
- * This is one of the most complicated components in this project.
- * Probably because the feature is complex and involved user inputs.
- *
- * I hope it contains following features:
- * - Input features
- *   - Supports a wide range of input types.
- *   - Validator to validate user inputs.
- *   - Can set initial values.
- *   - Dirty, aka, has user touched this input.
- * - Developer friendly
- *   - Easy to use APIs.
- *   - Type check as much as possible.
- * - UI
- *   - Configurable appearance.
- *   - Can display helper and error messages.
- * - Easy to extend, like new input types.
- *
- * So here is some design decisions:
- * Inputs are identified by its _key_.
- * `InputGroup` component takes care of only UI and no logic.
- * `useInputs` hook takes care of logic and generate props for `InputGroup`.
- */
-
-import { useState, Ref, useId } from "react";
-import classNames from "classnames";
-
-import { useC, Text, ThemeColor } from "../common";
-
-import "./InputGroup.css";
-
-export interface InputBase {
-  key: string;
-  label: Text;
-  helper?: Text;
-  disabled?: boolean;
-  error?: Text;
-}
-
-export interface TextInput extends InputBase {
-  type: "text";
-  value: string;
-  password?: boolean;
-}
-
-export interface BoolInput extends InputBase {
-  type: "bool";
-  value: boolean;
-}
-
-export interface SelectInputOption {
-  value: string;
-  label: Text;
-  icon?: string;
-}
-
-export interface SelectInput extends InputBase {
-  type: "select";
-  value: string;
-  options: SelectInputOption[];
-}
-
-export type Input = TextInput | BoolInput | SelectInput;
-
-export type InputValue = Input["value"];
-
-export type InputValueDict = Record<string, InputValue>;
-export type InputErrorDict = Record<string, Text>;
-export type InputDisabledDict = Record<string, boolean>;
-export type InputDirtyDict = Record<string, boolean>;
-
-export type GeneralInputErrorDict =
-  | {
-      [key: string]: Text | null | undefined;
-    }
-  | null
-  | undefined;
-
-type MakeInputInfo<I extends Input> = Omit<I, "value" | "error" | "disabled">;
-
-export type InputInfo = {
-  [I in Input as I["type"]]: MakeInputInfo<I>;
-}[Input["type"]];
-
-export type Validator = (
-  values: InputValueDict,
-  inputs: InputInfo[],
-) => GeneralInputErrorDict;
-
-export type InputScheme = {
-  inputs: InputInfo[];
-  validator?: Validator;
-};
-
-export type InputData = {
-  values: InputValueDict;
-  errors: InputErrorDict;
-  disabled: InputDisabledDict;
-  dirties: InputDirtyDict;
-};
-
-export type State = {
-  scheme: InputScheme;
-  data: InputData;
-};
-
-export type DataInitialization = {
-  values?: InputValueDict;
-  errors?: GeneralInputErrorDict;
-  disabled?: InputDisabledDict;
-  dirties?: InputDirtyDict;
-};
-
-export type Initialization = {
-  scheme: InputScheme;
-  dataInit?: DataInitialization;
-};
-
-export type GeneralInitialization = Initialization | InputScheme | InputInfo[];
-
-export type Initializer = GeneralInitialization | (() => GeneralInitialization);
-
-export interface InputGroupProps {
-  color?: ThemeColor;
-  containerClassName?: string;
-  containerRef?: Ref<HTMLDivElement>;
-
-  inputs: Input[];
-  onChange: (index: number, value: Input["value"]) => void;
-}
-
-function cleanObject<V>(o: Record<string, V>): Record<string, NonNullable<V>> {
-  const result = { ...o };
-  for (const key of Object.keys(result)) {
-    if (result[key] == null) {
-      delete result[key];
-    }
-  }
-  return result as never;
-}
-
-export type ConfirmResult =
-  | {
-      type: "ok";
-      values: InputValueDict;
-    }
-  | {
-      type: "error";
-      errors: InputErrorDict;
-    };
-
-function validate(
-  validator: Validator | null | undefined,
-  values: InputValueDict,
-  inputs: InputInfo[],
-): InputErrorDict {
-  return cleanObject(validator?.(values, inputs) ?? {});
-}
-
-export function useInputs(options: { init: Initializer }): {
-  inputGroupProps: InputGroupProps;
-  hasError: boolean;
-  hasErrorAndDirty: boolean;
-  confirm: () => ConfirmResult;
-  setAllDisabled: (disabled: boolean) => void;
-} {
-  function initializeValue(
-    input: InputInfo,
-    value?: InputValue | null,
-  ): InputValue {
-    if (input.type === "text") {
-      return value ?? "";
-    } else if (input.type === "bool") {
-      return value ?? false;
-    } else if (input.type === "select") {
-      return value ?? input.options[0].value;
-    }
-    throw new Error("Unknown input type");
-  }
-
-  function initialize(generalInitialization: GeneralInitialization): State {
-    const initialization: Initialization = Array.isArray(generalInitialization)
-      ? { scheme: { inputs: generalInitialization } }
-      : "scheme" in generalInitialization
-      ? generalInitialization
-      : { scheme: generalInitialization };
-
-    const { scheme, dataInit } = initialization;
-    const { inputs, validator } = scheme;
-    const keys = inputs.map((input) => input.key);
-
-    if (process.env.NODE_ENV === "development") {
-      const checkKeys = (dict: Record<string, unknown> | undefined) => {
-        if (dict != null) {
-          for (const key of Object.keys(dict)) {
-            if (!keys.includes(key)) {
-              console.warn("");
-            }
-          }
-        }
-      };
-
-      checkKeys(dataInit?.values);
-      checkKeys(dataInit?.errors ?? {});
-      checkKeys(dataInit?.disabled);
-      checkKeys(dataInit?.dirties);
-    }
-
-    function clean<V>(
-      dict: Record<string, V> | null | undefined,
-    ): Record<string, NonNullable<V>> {
-      return dict != null ? cleanObject(dict) : {};
-    }
-
-    const values: InputValueDict = {};
-    const disabled: InputDisabledDict = clean(dataInit?.disabled);
-    const dirties: InputDirtyDict = clean(dataInit?.dirties);
-    const isErrorSet = dataInit?.errors != null;
-    let errors: InputErrorDict = clean(dataInit?.errors);
-
-    for (let i = 0; i < inputs.length; i++) {
-      const input = inputs[i];
-      const { key } = input;
-
-      values[key] = initializeValue(input, dataInit?.values?.[key]);
-    }
-
-    if (isErrorSet) {
-      if (process.env.NODE_ENV === "development") {
-        console.log(
-          "You explicitly set errors (not undefined) in initializer, so validator won't run.",
-        );
-      }
-    } else {
-      errors = validate(validator, values, inputs);
-    }
-
-    return {
-      scheme,
-      data: {
-        values,
-        errors,
-        disabled,
-        dirties,
-      },
-    };
-  }
-
-  const { init } = options;
-  const initializer = typeof init === "function" ? init : () => init;
-
-  const [state, setState] = useState<State>(() => initialize(initializer()));
-
-  const { scheme, data } = state;
-  const { validator } = scheme;
-
-  function createAllBooleanDict(value: boolean): Record<string, boolean> {
-    const result: InputDirtyDict = {};
-    for (const key of scheme.inputs.map((input) => input.key)) {
-      result[key] = value;
-    }
-    return result;
-  }
-
-  const createAllDirties = () => createAllBooleanDict(true);
-
-  const componentInputs: Input[] = [];
-
-  for (let i = 0; i < scheme.inputs.length; i++) {
-    const input = scheme.inputs[i];
-    const value = data.values[input.key];
-    const error = data.errors[input.key];
-    const disabled = data.disabled[input.key] ?? false;
-    const dirty = data.dirties[input.key] ?? false;
-    const componentInput: Input = {
-      ...input,
-      value: value as never,
-      disabled,
-      error: dirty ? error : undefined,
-    };
-    componentInputs.push(componentInput);
-  }
-
-  const hasError = Object.keys(data.errors).length > 0;
-  const hasDirty = Object.keys(data.dirties).some((key) => data.dirties[key]);
-
-  return {
-    inputGroupProps: {
-      inputs: componentInputs,
-      onChange: (index, value) => {
-        const input = scheme.inputs[index];
-        const { key } = input;
-        const newValues = { ...data.values, [key]: value };
-        const newDirties = { ...data.dirties, [key]: true };
-        const newErrors = validate(validator, newValues, scheme.inputs);
-        setState({
-          scheme,
-          data: {
-            ...data,
-            values: newValues,
-            errors: newErrors,
-            dirties: newDirties,
-          },
-        });
-      },
-    },
-    hasError,
-    hasErrorAndDirty: hasError && hasDirty,
-    confirm() {
-      const newDirties = createAllDirties();
-      const newErrors = validate(validator, data.values, scheme.inputs);
-
-      setState({
-        scheme,
-        data: {
-          ...data,
-          dirties: newDirties,
-          errors: newErrors,
-        },
-      });
-
-      if (Object.keys(newErrors).length !== 0) {
-        return {
-          type: "error",
-          errors: newErrors,
-        };
-      } else {
-        return {
-          type: "ok",
-          values: data.values,
-        };
-      }
-    },
-    setAllDisabled(disabled: boolean) {
-      setState({
-        scheme,
-        data: {
-          ...data,
-          disabled: createAllBooleanDict(disabled),
-        },
-      });
-    },
-  };
-}
-
-export function InputGroup({
-  color,
-  inputs,
-  onChange,
-  containerRef,
-  containerClassName,
-}: InputGroupProps) {
-  const c = useC();
-
-  const id = useId();
-
-  return (
-    <div
-      ref={containerRef}
-      className={classNames(
-        "cru-input-group",
-        `cru-clickable-${color ?? "primary"}`,
-        containerClassName,
-      )}
-    >
-      {inputs.map((item, index) => {
-        const { key, type, value, label, error, helper, disabled } = item;
-
-        const getContainerClassName = (
-          ...additionalClassNames: classNames.ArgumentArray
-        ) =>
-          classNames(
-            `cru-input-container cru-input-type-${type}`,
-            error && "error",
-            ...additionalClassNames,
-          );
-
-        const changeValue = (value: InputValue) => {
-          onChange(index, value);
-        };
-
-        const inputId = `${id}-${key}`;
-
-        if (type === "text") {
-          const { password } = item;
-          return (
-            <div
-              key={key}
-              className={getContainerClassName(password && "password")}
-            >
-              {label && (
-                <label className="cru-input-label" htmlFor={inputId}>
-                  {c(label)}
-                </label>
-              )}
-              <input
-                id={inputId}
-                type={password ? "password" : "text"}
-                value={value}
-                onChange={(event) => {
-                  const v = event.target.value;
-                  changeValue(v);
-                }}
-                disabled={disabled}
-              />
-              {error && <div className="cru-input-error">{c(error)}</div>}
-              {helper && <div className="cru-input-helper">{c(helper)}</div>}
-            </div>
-          );
-        } else if (type === "bool") {
-          return (
-            <div key={key} className={getContainerClassName()}>
-              <input
-                id={inputId}
-                type="checkbox"
-                checked={value}
-                onChange={(event) => {
-                  const v = event.currentTarget.checked;
-                  changeValue(v);
-                }}
-                disabled={disabled}
-              />
-              <label className="cru-input-label-inline" htmlFor={inputId}>
-                {c(label)}
-              </label>
-              {error && <div className="cru-input-error">{c(error)}</div>}
-              {helper && <div className="cru-input-helper">{c(helper)}</div>}
-            </div>
-          );
-        } else if (type === "select") {
-          return (
-            <div key={key} className={getContainerClassName()}>
-              <label className="cru-input-label" htmlFor={inputId}>
-                {c(label)}
-              </label>
-              <select
-                id={inputId}
-                value={value}
-                onChange={(event) => {
-                  const e = event.target.value;
-                  changeValue(e);
-                }}
-                disabled={disabled}
-              >
-                {item.options.map((option) => {
-                  return (
-                    <option value={option.value} key={option.value}>
-                      {option.icon}
-                      {c(option.label)}
-                    </option>
-                  );
-                })}
-              </select>
-            </div>
-          );
-        }
-      })}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/list/ListContainer.css b/FrontEnd/src/views/common/list/ListContainer.css
deleted file mode 100644
index 53781834..00000000
--- a/FrontEnd/src/views/common/list/ListContainer.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.cru-list-container {
-  border: 1px solid var(--cru-clickable-primary-normal-color);
-  border-radius: 5px;
-}
diff --git a/FrontEnd/src/views/common/list/ListContainer.tsx b/FrontEnd/src/views/common/list/ListContainer.tsx
deleted file mode 100644
index aa00d12c..00000000
--- a/FrontEnd/src/views/common/list/ListContainer.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ComponentPropsWithoutRef, forwardRef, Ref } from "react";
-import classNames from "classnames";
-
-import "./ListContainer.css"
-
-function _ListContainer(
-  { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">,
-  ref: Ref<HTMLDivElement>,
-) {
-  return (
-    <div
-      ref={ref}
-      className={classNames("cru-list-container", className)}
-      {...otherProps}
-    >
-      {children}
-    </div>
-  );
-}
-
-const ListContainer = forwardRef(_ListContainer);
-
-export default ListContainer;
diff --git a/FrontEnd/src/views/common/list/ListItemContainer.css b/FrontEnd/src/views/common/list/ListItemContainer.css
deleted file mode 100644
index 8d7afa9f..00000000
--- a/FrontEnd/src/views/common/list/ListItemContainer.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.cru-list-item-container {
-  border: 1px solid var(--cru-clickable-primary-normal-color);
-}
diff --git a/FrontEnd/src/views/common/list/ListItemContainer.tsx b/FrontEnd/src/views/common/list/ListItemContainer.tsx
deleted file mode 100644
index 315cbd6e..00000000
--- a/FrontEnd/src/views/common/list/ListItemContainer.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ComponentPropsWithoutRef, forwardRef, Ref } from "react";
-import classNames from "classnames";
-
-import "./ListItemContainer.css";
-
-function _ListItemContainer(
-  { className, children, ...otherProps }: ComponentPropsWithoutRef<"div">,
-  ref: Ref<HTMLDivElement>,
-) {
-  return (
-    <div
-      ref={ref}
-      className={classNames("cru-list-item-container", className)}
-      {...otherProps}
-    >
-      {children}
-    </div>
-  );
-}
-
-const ListItemContainer = forwardRef(_ListItemContainer);
-
-export default ListItemContainer;
diff --git a/FrontEnd/src/views/common/list/index.ts b/FrontEnd/src/views/common/list/index.ts
deleted file mode 100644
index e183f7da..00000000
--- a/FrontEnd/src/views/common/list/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import ListContainer from "./ListContainer";
-import ListItemContainer from "./ListItemContainer";
-
-export { ListContainer, ListItemContainer };
diff --git a/FrontEnd/src/views/common/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css
deleted file mode 100644
index 75734533..00000000
--- a/FrontEnd/src/views/common/menu/Menu.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.cru-menu {
-  min-width: 200px;
-}
-
-.cru-menu-item {
-  display: block;
-  font-size: 1em;
-  width: 100%;
-  padding: 0.5em 1.5em;
-  transition: all 0.5s;
-  color: var(--cru-clickable-normal-color);
-  background-color: var(--cru-clickable-grayscale-normal-color);
-  border: none;
-  cursor: pointer;
-}
-
-.cru-menu-item:hover {
-  background-color: var(--cru-clickable-grayscale-hover-color);
-}
-
-.cru-menu-item:focus {
-  background-color: var(--cru-clickable-grayscale-focus-color);
-}
-
-.cru-menu-item:active {
-  background-color: var(--cru-clickable-grayscale-active-color);
-}
-
-.cru-menu-item-icon {
-  margin-right: 1em;
-}
-
-.cru-menu-divider {
-  border-width: 0;
-  border-top: 1px solid var(--cru-primary-color);
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/menu/Menu.tsx b/FrontEnd/src/views/common/menu/Menu.tsx
deleted file mode 100644
index e8099c76..00000000
--- a/FrontEnd/src/views/common/menu/Menu.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { CSSProperties } from "react";
-import classNames from "classnames";
-
-import { useC, Text, ThemeColor } from "../common";
-
-import "./Menu.css";
-import Icon from "../Icon";
-
-export type MenuItem =
-  | {
-      type: "divider";
-    }
-  | {
-      type: "button";
-      text: Text;
-      icon?: string;
-      color?: ThemeColor;
-      onClick: () => void;
-    };
-
-export type MenuItems = MenuItem[];
-
-export type MenuProps = {
-  items: MenuItems;
-  onItemClicked?: () => void;
-  className?: string;
-  style?: CSSProperties;
-};
-
-export default function Menu({
-  items,
-  onItemClicked,
-  className,
-  style,
-}: MenuProps) {
-  const c = useC();
-
-  return (
-    <div className={classNames("cru-menu", className)} style={style}>
-      {items.map((item, index) => {
-        if (item.type === "divider") {
-          return <hr key={index} className="cru-menu-divider" />;
-        } else {
-          const { text, color, icon, onClick } = item;
-          return (
-            <button
-              key={index}
-              className={`cru-menu-item cru-clickable-${color ?? "primary"}`}
-              onClick={() => {
-                onClick();
-                onItemClicked?.();
-              }}
-            >
-              {icon != null && <Icon color={color} icon={icon} />}
-              {c(text)}
-            </button>
-          );
-        }
-      })}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.css b/FrontEnd/src/views/common/menu/PopupMenu.css
deleted file mode 100644
index 149e0699..00000000
--- a/FrontEnd/src/views/common/menu/PopupMenu.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.cru-popup-menu-menu-container {
-  z-index: 1040;
-  border-radius: 3px;
-  border: var(--cru-clickable-normal-color) 1.5px solid;
-  background-color: var(--cru-background-color);
-  overflow: hidden;
-}
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.tsx b/FrontEnd/src/views/common/menu/PopupMenu.tsx
deleted file mode 100644
index 5c8d5e98..00000000
--- a/FrontEnd/src/views/common/menu/PopupMenu.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { useState, CSSProperties, ReactNode } from "react";
-import classNames from "classnames";
-import { createPortal } from "react-dom";
-import { usePopper } from "react-popper";
-
-import { useClickOutside } from "@/utilities/hooks";
-
-import Menu, { MenuItems } from "./Menu";
-
-import { ThemeColor } from "../common";
-
-import "./PopupMenu.css";
-
-export interface PopupMenuProps {
-  color?: ThemeColor;
-  items: MenuItems;
-  children?: ReactNode;
-  containerClassName?: string;
-  containerStyle?: CSSProperties;
-}
-
-export default function PopupMenu({
-  color,
-  items,
-  children,
-  containerClassName,
-  containerStyle,
-}: PopupMenuProps) {
-  const [show, setShow] = useState<boolean>(false);
-
-  const [referenceElement, setReferenceElement] =
-    useState<HTMLDivElement | null>(null);
-  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
-    null,
-  );
-  const { styles, attributes } = usePopper(referenceElement, popperElement);
-
-  useClickOutside(popperElement, () => setShow(false), true);
-
-  return (
-    <div
-      ref={setReferenceElement}
-      className={classNames(
-        "cru-popup-menu-trigger-container",
-        containerClassName,
-      )}
-      style={containerStyle}
-      onClick={() => setShow(true)}
-    >
-      {children}
-      {show &&
-        createPortal(
-          <div
-            ref={setPopperElement}
-            className={`cru-popup-menu-menu-container cru-clickable-${
-              color ?? "primary"
-            }`}
-            style={styles.popper}
-            {...attributes.popper}
-          >
-            <Menu
-              items={items}
-              onItemClicked={() => {
-                setShow(false);
-              }}
-            />
-          </div>,
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          document.getElementById("portal")!,
-        )}
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/tab/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx
deleted file mode 100644
index cdb988e0..00000000
--- a/FrontEnd/src/views/common/tab/TabPages.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as React from "react";
-
-import { I18nText, UiLogicError } from "@/common";
-
-import Tabs from "./Tabs";
-
-export interface TabPage {
-  name: string;
-  text: I18nText;
-  page: React.ReactNode;
-}
-
-export interface TabPagesProps {
-  pages: TabPage[];
-  actions?: React.ReactNode;
-  dense?: boolean;
-  className?: string;
-  style?: React.CSSProperties;
-  navClassName?: string;
-  navStyle?: React.CSSProperties;
-  pageContainerClassName?: string;
-  pageContainerStyle?: React.CSSProperties;
-}
-
-const TabPages: React.FC<TabPagesProps> = ({
-  pages,
-  actions,
-  dense,
-  className,
-  style,
-  navClassName,
-  navStyle,
-  pageContainerClassName,
-  pageContainerStyle,
-}) => {
-  if (pages.length === 0) {
-    throw new UiLogicError("Page list can't be empty.");
-  }
-
-  const [tab, setTab] = React.useState<string>(pages[0].name);
-
-  const currentPage = pages.find((p) => p.name === tab);
-
-  if (currentPage == null) {
-    throw new UiLogicError("Current tab value is bad.");
-  }
-
-  return (
-    <div className={className} style={style}>
-      <Tabs
-        tabs={pages.map((page) => ({
-          name: page.name,
-          text: page.text,
-          onClick: () => {
-            setTab(page.name);
-          },
-        }))}
-        dense={dense}
-        activeTabName={tab}
-        className={navClassName}
-        style={navStyle}
-        actions={actions}
-      />
-      <div className={pageContainerClassName} style={pageContainerStyle}>
-        {currentPage.page}
-      </div>
-    </div>
-  );
-};
-
-export default TabPages;
diff --git a/FrontEnd/src/views/common/tab/Tabs.css b/FrontEnd/src/views/common/tab/Tabs.css
deleted file mode 100644
index 395d16a7..00000000
--- a/FrontEnd/src/views/common/tab/Tabs.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.cru-nav {
-  border-bottom: var(--cru-primary-color) 1px solid;
-  display: flex;
-}
-
-.cru-nav-item {
-  color: var(--cru-primary-color);
-  border: var(--cru-background-2-color) 0.5px solid;
-  border-bottom: none;
-  padding: 0.5em 1.5em;
-  border-top-left-radius: 5px;
-  border-top-right-radius: 5px;
-  transition: all 0.5s;
-  cursor: pointer;
-}
-
-.cru-nav.dense .cru-nav-item {
-  padding: 0.2em 1em;
-}
-
-.cru-nav-item:hover {
-  background-color: var(--cru-background-1-color);
-}
-
-.cru-nav-item.active {
-  color: var(--cru-primary-t-color);
-  background-color: var(--cru-primary-color);
-  border-color: var(--cru-primary-color);
-}
-
-.cru-nav-action-area {
-  margin-left: auto;
-}
diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx
deleted file mode 100644
index 3e3ef6fa..00000000
--- a/FrontEnd/src/views/common/tab/Tabs.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import * as React from "react";
-import { Link } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import classnames from "classnames";
-
-import { convertI18nText, I18nText } from "@/common";
-
-import "./Tabs.css";
-
-export interface Tab {
-  name: string;
-  text: I18nText;
-  link?: string;
-  onClick?: () => void;
-}
-
-export interface TabsProps {
-  activeTabName?: string;
-  actions?: React.ReactNode;
-  dense?: boolean;
-  tabs: Tab[];
-  className?: string;
-  style?: React.CSSProperties;
-}
-
-export default function Tabs(props: TabsProps): React.ReactElement | null {
-  const { tabs, activeTabName, className, style, dense, actions } = props;
-
-  const { t } = useTranslation();
-
-  return (
-    <div
-      className={classnames("cru-nav", dense && "dense", className)}
-      style={style}
-    >
-      {tabs.map((tab) => {
-        const active = activeTabName === tab.name;
-        const className = classnames("cru-nav-item", active && "active");
-
-        if (tab.link != null) {
-          return (
-            <Link
-              key={tab.name}
-              to={tab.link}
-              onClick={tab.onClick}
-              className={className}
-            >
-              {convertI18nText(tab.text, t)}
-            </Link>
-          );
-        } else {
-          return (
-            <span key={tab.name} onClick={tab.onClick} className={className}>
-              {convertI18nText(tab.text, t)}
-            </span>
-          );
-        }
-      })}
-      <div className="cru-nav-action-area">{actions}</div>
-    </div>
-  );
-}
diff --git a/FrontEnd/src/views/common/theme-color.css b/FrontEnd/src/views/common/theme-color.css
deleted file mode 100644
index 24a7e267..00000000
--- a/FrontEnd/src/views/common/theme-color.css
+++ /dev/null
@@ -1,173 +0,0 @@
-/* Generated by theme-generator.ts */
-
-:root {
-  --cru-primary-color: hsl(210 100% 40%);
-  --cru-primary-1-color: hsl(210 100% 37%);
-  --cru-primary-2-color: hsl(210 100% 34%);
-  --cru-primary-on-color: hsl(210 100% 100%);
-  --cru-primary-container-color: hsl(210 100% 90%);
-  --cru-primary-container-1-color: hsl(210 100% 80%);
-  --cru-primary-container-2-color: hsl(210 100% 70%);
-  --cru-primary-on-container-color: hsl(210 100% 10%);
-  --cru-secondary-color: hsl(40 100% 40%);
-  --cru-secondary-1-color: hsl(40 100% 37%);
-  --cru-secondary-2-color: hsl(40 100% 34%);
-  --cru-secondary-on-color: hsl(40 100% 100%);
-  --cru-secondary-container-color: hsl(40 100% 90%);
-  --cru-secondary-container-1-color: hsl(40 100% 80%);
-  --cru-secondary-container-2-color: hsl(40 100% 70%);
-  --cru-secondary-on-container-color: hsl(40 100% 10%);
-  --cru-tertiary-color: hsl(160 100% 40%);
-  --cru-tertiary-1-color: hsl(160 100% 37%);
-  --cru-tertiary-2-color: hsl(160 100% 34%);
-  --cru-tertiary-on-color: hsl(160 100% 100%);
-  --cru-tertiary-container-color: hsl(160 100% 90%);
-  --cru-tertiary-container-1-color: hsl(160 100% 80%);
-  --cru-tertiary-container-2-color: hsl(160 100% 70%);
-  --cru-tertiary-on-container-color: hsl(160 100% 10%);
-  --cru-danger-color: hsl(0 100% 40%);
-  --cru-danger-1-color: hsl(0 100% 37%);
-  --cru-danger-2-color: hsl(0 100% 34%);
-  --cru-danger-on-color: hsl(0 100% 100%);
-  --cru-danger-container-color: hsl(0 100% 90%);
-  --cru-danger-container-1-color: hsl(0 100% 80%);
-  --cru-danger-container-2-color: hsl(0 100% 70%);
-  --cru-danger-on-container-color: hsl(0 100% 10%);
-  --cru-success-color: hsl(120 60% 40%);
-  --cru-success-1-color: hsl(120 60% 37%);
-  --cru-success-2-color: hsl(120 60% 34%);
-  --cru-success-on-color: hsl(120 60% 100%);
-  --cru-success-container-color: hsl(120 60% 90%);
-  --cru-success-container-1-color: hsl(120 60% 80%);
-  --cru-success-container-2-color: hsl(120 60% 70%);
-  --cru-success-on-container-color: hsl(120 60% 10%);
-  --cru-surface-dim-color: hsl(0 0% 87%);
-  --cru-surface-color: hsl(0 0% 98%);
-  --cru-surface-1-color: hsl(0 0% 90%);
-  --cru-surface-2-color: hsl(0 0% 82%);
-  --cru-surface-bright-color: hsl(0 0% 98%);
-  --cru-surface-container-lowest-color: hsl(0 0% 100%);
-  --cru-surface-container-low-color: hsl(0 0% 96%);
-  --cru-surface-container-color: hsl(0 0% 94%);
-  --cru-surface-container-high-color: hsl(0 0% 92%);
-  --cru-surface-container-highest-color: hsl(0 0% 90%);
-  --cru-surface-on-color: hsl(0 0% 10%);
-  --cru-surface-on-variant-color: hsl(0 0% 30%);
-  --cru-surface-outline-color: hsl(0 0% 50%);
-  --cru-surface-outline-variant-color: hsl(0 0% 80%);
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --cru-primary-color: hsl(210 100% 80%);
-    --cru-primary-1-color: hsl(210 100% 75%);
-    --cru-primary-2-color: hsl(210 100% 68%);
-    --cru-primary-on-color: hsl(210 100% 20%);
-    --cru-primary-container-color: hsl(210 100% 30%);
-    --cru-primary-container-1-color: hsl(210 100% 25%);
-    --cru-primary-container-2-color: hsl(210 100% 20%);
-    --cru-primary-on-container-color: hsl(210 100% 90%);
-    --cru-secondary-color: hsl(40 100% 80%);
-    --cru-secondary-1-color: hsl(40 100% 75%);
-    --cru-secondary-2-color: hsl(40 100% 68%);
-    --cru-secondary-on-color: hsl(40 100% 20%);
-    --cru-secondary-container-color: hsl(40 100% 30%);
-    --cru-secondary-container-1-color: hsl(40 100% 25%);
-    --cru-secondary-container-2-color: hsl(40 100% 20%);
-    --cru-secondary-on-container-color: hsl(40 100% 90%);
-    --cru-tertiary-color: hsl(160 100% 80%);
-    --cru-tertiary-1-color: hsl(160 100% 75%);
-    --cru-tertiary-2-color: hsl(160 100% 68%);
-    --cru-tertiary-on-color: hsl(160 100% 20%);
-    --cru-tertiary-container-color: hsl(160 100% 30%);
-    --cru-tertiary-container-1-color: hsl(160 100% 25%);
-    --cru-tertiary-container-2-color: hsl(160 100% 20%);
-    --cru-tertiary-on-container-color: hsl(160 100% 90%);
-    --cru-danger-color: hsl(0 100% 80%);
-    --cru-danger-1-color: hsl(0 100% 75%);
-    --cru-danger-2-color: hsl(0 100% 68%);
-    --cru-danger-on-color: hsl(0 100% 20%);
-    --cru-danger-container-color: hsl(0 100% 30%);
-    --cru-danger-container-1-color: hsl(0 100% 25%);
-    --cru-danger-container-2-color: hsl(0 100% 20%);
-    --cru-danger-on-container-color: hsl(0 100% 90%);
-    --cru-success-color: hsl(120 60% 80%);
-    --cru-success-1-color: hsl(120 60% 75%);
-    --cru-success-2-color: hsl(120 60% 68%);
-    --cru-success-on-color: hsl(120 60% 20%);
-    --cru-success-container-color: hsl(120 60% 30%);
-    --cru-success-container-1-color: hsl(120 60% 25%);
-    --cru-success-container-2-color: hsl(120 60% 20%);
-    --cru-success-on-container-color: hsl(120 60% 90%);
-    --cru-surface-dim-color: hsl(0 0% 6%);
-    --cru-surface-color: hsl(0 0% 6%);
-    --cru-surface-1-color: hsl(0 0% 25%);
-    --cru-surface-2-color: hsl(0 0% 40%);
-    --cru-surface-bright-color: hsl(0 0% 24%);
-    --cru-surface-container-lowest-color: hsl(0 0% 4%);
-    --cru-surface-container-low-color: hsl(0 0% 10%);
-    --cru-surface-container-color: hsl(0 0% 12%);
-    --cru-surface-container-high-color: hsl(0 0% 17%);
-    --cru-surface-container-highest-color: hsl(0 0% 22%);
-    --cru-surface-on-color: hsl(0 0% 90%);
-    --cru-surface-on-variant-color: hsl(0 0% 80%);
-    --cru-surface-outline-color: hsl(0 0% 60%);
-    --cru-surface-outline-variant-color: hsl(0 0% 30%);
-  }
-}
-
-.cru-primary {
-  --cru-key-color: var(--cru-primary-color);
-  --cru-key-1-color: var(--cru-primary-1-color);
-  --cru-key-2-color: var(--cru-primary-2-color);
-  --cru-key-on-color: var(--cru-primary-on-color);
-  --cru-key-container-color: var(--cru-primary-container-color);
-  --cru-key-container-1-color: var(--cru-primary-container-1-color);
-  --cru-key-container-2-color: var(--cru-primary-container-2-color);
-  --cru-key-on-container-color: var(--cru-primary-on-container-color);
-}
-
-.cru-secondary {
-  --cru-key-color: var(--cru-secondary-color);
-  --cru-key-1-color: var(--cru-secondary-1-color);
-  --cru-key-2-color: var(--cru-secondary-2-color);
-  --cru-key-on-color: var(--cru-secondary-on-color);
-  --cru-key-container-color: var(--cru-secondary-container-color);
-  --cru-key-container-1-color: var(--cru-secondary-container-1-color);
-  --cru-key-container-2-color: var(--cru-secondary-container-2-color);
-  --cru-key-on-container-color: var(--cru-secondary-on-container-color);
-}
-
-.cru-tertiary {
-  --cru-key-color: var(--cru-tertiary-color);
-  --cru-key-1-color: var(--cru-tertiary-1-color);
-  --cru-key-2-color: var(--cru-tertiary-2-color);
-  --cru-key-on-color: var(--cru-tertiary-on-color);
-  --cru-key-container-color: var(--cru-tertiary-container-color);
-  --cru-key-container-1-color: var(--cru-tertiary-container-1-color);
-  --cru-key-container-2-color: var(--cru-tertiary-container-2-color);
-  --cru-key-on-container-color: var(--cru-tertiary-on-container-color);
-}
-
-.cru-danger {
-  --cru-key-color: var(--cru-danger-color);
-  --cru-key-1-color: var(--cru-danger-1-color);
-  --cru-key-2-color: var(--cru-danger-2-color);
-  --cru-key-on-color: var(--cru-danger-on-color);
-  --cru-key-container-color: var(--cru-danger-container-color);
-  --cru-key-container-1-color: var(--cru-danger-container-1-color);
-  --cru-key-container-2-color: var(--cru-danger-container-2-color);
-  --cru-key-on-container-color: var(--cru-danger-on-container-color);
-}
-
-.cru-success {
-  --cru-key-color: var(--cru-success-color);
-  --cru-key-1-color: var(--cru-success-1-color);
-  --cru-key-2-color: var(--cru-success-2-color);
-  --cru-key-on-color: var(--cru-success-on-color);
-  --cru-key-container-color: var(--cru-success-container-color);
-  --cru-key-container-1-color: var(--cru-success-container-1-color);
-  --cru-key-container-2-color: var(--cru-success-container-2-color);
-  --cru-key-on-container-color: var(--cru-success-on-container-color);
-}
-
diff --git a/FrontEnd/src/views/common/theme.css b/FrontEnd/src/views/common/theme.css
deleted file mode 100644
index 6ceb369f..00000000
--- a/FrontEnd/src/views/common/theme.css
+++ /dev/null
@@ -1,146 +0,0 @@
-@import "./theme-color.css";
-
-:root {
-  --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-  --cru-page-padding: 1em 2em;
-
-  --cru-border-radius: 4px;
-  --cru-card-border-radius: 4px;
-}
-
-/* theme colors */
-:root {
-  --cru-primary-color: hsl(210, 100%, 50%);
-  --cru-secondary-color: hsl(30, 100%, 50%);
-  --cru-create-color: hsl(120, 100%, 25%);
-  --cru-danger-color: hsl(0, 100%, 50%);
-}
-
-/* common colors */
-:root {
-  --cru-background-color: hsl(0, 0%, 100%);
-  --cru-container-background-color: hsl(0, 0%, 97%);
-  --cru-text-primary-color: hsl(0, 0%, 0%);
-  --cru-text-secondary-color: hsl(0, 0%, 38%);
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --cru-background-color: hsl(0, 0%, 0%);
-    --cru-container-background-color: hsl(0, 0%, 2%);
-    --cru-text-primary-color: hsl(0, 0%, 100%);
-    --cru-text-secondary-color: hsl(0, 0%, 85%);
-  }
-}
-
-:root {
-  --cru-body-background-color: var(--cru-background-color);
-}
-
-/* clickable color */
-:root {
-  --cru-clickable-primary-normal-color: var(--cru-primary-color);
-  --cru-clickable-primary-hover-color: hsl(210, 100%, 60%);
-  --cru-clickable-primary-focus-color: hsl(210, 100%, 60%);
-  --cru-clickable-primary-active-color: hsl(210, 100%, 70%);
-  --cru-clickable-secondary-normal-color: var(--cru-secondary-color);
-  --cru-clickable-secondary-hover-color: hsl(30, 100%, 60%);
-  --cru-clickable-secondary-focus-color: hsl(30, 100%, 60%);
-  --cru-clickable-secondary-active-color: hsl(30, 100%, 70%);
-  --cru-clickable-create-normal-color: var(--cru-create-color);
-  --cru-clickable-create-hover-color: hsl(120, 100%, 35%);
-  --cru-clickable-create-focus-color: hsl(120, 100%, 35%);
-  --cru-clickable-create-active-color: hsl(120, 100%, 35%);
-  --cru-clickable-danger-normal-color: var(--cru-danger-color);
-  --cru-clickable-danger-hover-color: hsl(0, 100%, 60%);
-  --cru-clickable-danger-focus-color: hsl(0, 100%, 60%);
-  --cru-clickable-danger-active-color: hsl(0, 100%, 70%);
-  --cru-clickable-grayscale-normal-color: hsl(0, 0%, 100%);
-  --cru-clickable-grayscale-hover-color: hsl(0, 0%, 92%);
-  --cru-clickable-grayscale-focus-color: hsl(0, 0%, 92%);
-  --cru-clickable-grayscale-active-color: hsl(0, 0%, 88%);
-  --cru-clickable-disabled-color: hsl(0, 0%, 50%);
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --cru-clickable-grayscale-normal-color: hsl(0, 0%, 0%);
-    --cru-clickable-grayscale-hover-color: hsl(0, 0%, 10%);
-    --cru-clickable-grayscale-focus-color: hsl(0, 0%, 10%);
-    --cru-clickable-grayscale-active-color: hsl(0, 0%, 20%);
-  }
-}
-
-.cru-clickable-primary {
-  --cru-clickable-normal-color: var(--cru-clickable-primary-normal-color);
-  --cru-clickable-hover-color: var(--cru-clickable-primary-hover-color);
-  --cru-clickable-focus-color: var(--cru-clickable-primary-focus-color);
-  --cru-clickable-active-color: var(--cru-clickable-primary-active-color);
-}
-
-.cru-clickable-secondary {
-  --cru-clickable-normal-color: var(--cru-clickable-secondary-normal-color);
-  --cru-clickable-hover-color: var(--cru-clickable-secondary-hover-color);
-  --cru-clickable-focus-color: var(--cru-clickable-secondary-focus-color);
-  --cru-clickable-active-color: var(--cru-clickable-secondary-active-color);
-}
-
-.cru-clickable-create {
-  --cru-clickable-normal-color: var(--cru-clickable-create-normal-color);
-  --cru-clickable-hover-color: var(--cru-clickable-create-hover-color);
-  --cru-clickable-focus-color: var(--cru-clickable-create-focus-color);
-  --cru-clickable-active-color: var(--cru-clickable-create-active-color);
-}
-
-.cru-clickable-danger {
-  --cru-clickable-normal-color: var(--cru-clickable-danger-normal-color);
-  --cru-clickable-hover-color: var(--cru-clickable-danger-hover-color);
-  --cru-clickable-focus-color: var(--cru-clickable-danger-focus-color);
-  --cru-clickable-active-color: var(--cru-clickable-danger-active-color);
-}
-
-.cru-clickable-grayscale {
-  --cru-clickable-normal-color: var(--cru-clickable-grayscale-normal-color);
-  --cru-clickable-hover-color: var(--cru-clickable-grayscale-hover-color);
-  --cru-clickable-focus-color: var(--cru-clickable-grayscale-focus-color);
-  --cru-clickable-active-color: var(--cru-clickable-grayscale-active-color);
-}
-
-/* button colors */
-:root {
-  /* push button colors */
-  --cru-push-button-text-color: #ffffff;
-  --cru-push-button-disabled-text-color: hsl(0, 0%, 80%);
-}
-
-/* Card colors */
-:root {
-  --cru-card-background-primary-color: hsl(210, 100%, 50%);
-  --cru-card-border-primary-color: hsl(210, 100%, 50%);
-  --cru-card-background-secondary-color: hsl(30, 100%, 50%);
-  --cru-card-border-secondary-color: hsl(30, 100%, 50%);
-  --cru-card-background-create-color: hsl(120, 100%, 25%);
-  --cru-card-border-create-color: hsl(120, 100%, 25%);
-  --cru-card-background-danger-color: hsl(0, 100%, 50%);
-  --cru-card-border-danger-color: hsl(0, 100%, 50%);
-}
-
-.cru-card-primary {
-  --cru-card-background-color: var(--cru-card-background-primary-color);
-  --cru-card-border-color: var(--cru-card-border-primary-color)
-}
-
-.cru-card-secondary {
-  --cru-card-background-color: var(--cru-card-background-secondary-color);
-  --cru-card-border-color: var(--cru-card-border-secondary-color)
-}
-
-.cru-card-create {
-  --cru-card-background-color: var(--cru-card-background-create-color);
-  --cru-card-border-color: var(--cru-card-border-create-color)
-}
-
-.cru-card-danger {
-  --cru-card-background-color: var(--cru-card-background-danger-color);
-  --cru-card-border-color: var(--cru-card-border-danger-color)
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx
deleted file mode 100644
index aea7bd48..00000000
--- a/FrontEnd/src/views/common/user/UserAvatar.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Ref, ComponentPropsWithoutRef } from "react";
-
-import { getHttpUserClient } from "@/http/user";
-
-export interface UserAvatarProps extends ComponentPropsWithoutRef<"img"> {
-  username: string;
-  imgRef?: Ref<HTMLImageElement> | null;
-}
-
-export default function UserAvatar({
-  username,
-  imgRef,
-  ...otherProps
-}: UserAvatarProps) {
-  return (
-    <img
-      ref={imgRef}
-      src={getHttpUserClient().generateAvatarUrl(username)}
-      {...otherProps}
-    />
-  );
-}
diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json
index cd6aa25d..bf106d95 100644
--- a/FrontEnd/tsconfig.json
+++ b/FrontEnd/tsconfig.json
@@ -21,9 +21,6 @@
       "~*": [
         "./*"
       ],
-      "@/*": [
-        "./src/*"
-      ]
     },
     "noEmit": true
   },
-- 
cgit v1.2.3


From e06f1a33e678ac264f9290013f49237ea97e699b Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Wed, 20 Sep 2023 19:37:02 +0800
Subject: ...

---
 FrontEnd/package.json                              |   54 +-
 FrontEnd/pnpm-lock.yaml                            | 1600 +++++++++++++-------
 FrontEnd/src/components/dialog/ConfirmDialog.tsx   |   25 +-
 FrontEnd/src/components/dialog/DialogContainer.css |    3 +-
 FrontEnd/src/pages/timeline/Timeline.css           |   19 +-
 FrontEnd/src/pages/timeline/TimelineDateLabel.css  |    6 +-
 FrontEnd/src/pages/timeline/TimelinePostList.tsx   |    1 -
 7 files changed, 1085 insertions(+), 623 deletions(-)

(limited to 'FrontEnd/src/pages/timeline/TimelinePostList.tsx')

diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index 6bb8b6e4..4a94d319 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -12,25 +12,25 @@
     "check:fix": "pnpm run type-check && pnpm run lint:fix"
   },
   "dependencies": {
-    "@floating-ui/react-dom": "^2.0.1",
-    "@microsoft/signalr": "^7.0.7",
-    "axios": "^1.4.0",
-    "bootstrap": "^5.3.0",
-    "bootstrap-icons": "^1.10.5",
+    "@floating-ui/react-dom": "^2.0.2",
+    "@microsoft/signalr": "^7.0.11",
+    "axios": "^1.5.0",
+    "bootstrap": "^5.3.2",
+    "bootstrap-icons": "^1.11.1",
     "classnames": "^2.3.2",
     "color": "^4.2.3",
-    "core-js": "^3.31.1",
-    "i18next": "^23.2.8",
+    "core-js": "^3.32.2",
+    "i18next": "^23.5.1",
     "i18next-browser-languagedetector": "^7.1.0",
     "lodash": "^4.17.21",
-    "marked": "^5.1.1",
+    "marked": "^9.0.3",
     "moment": "^2.29.4",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "react-i18next": "^13.0.1",
+    "react-i18next": "^13.2.2",
     "react-popper": "^2.3.0",
     "react-responsive": "^9.0.2",
-    "react-router-dom": "^6.14.1",
+    "react-router-dom": "^6.16.0",
     "react-transition-group": "^4.4.5",
     "rxjs": "^7.8.1",
     "xregexp": "^5.1.1"
@@ -38,30 +38,30 @@
   "devDependencies": {
     "@parcel/packager-raw-url": "2.9.3",
     "@parcel/transformer-webmanifest": "2.9.3",
-    "@tsconfig/node20": "^1.0.2",
-    "@types/color": "^3.0.3",
-    "@types/lodash": "^4.14.195",
-    "@types/marked": "^5.0.0",
-    "@types/node": "^20.4.1",
+    "@tsconfig/node20": "^20.1.2",
+    "@types/color": "^3.0.4",
+    "@types/lodash": "^4.14.198",
+    "@types/marked": "^5.0.1",
+    "@types/node": "^20.6.3",
     "@types/parcel-env": "^0.0.1",
-    "@types/react": "^18.2.14",
+    "@types/react": "^18.2.22",
     "@types/react-color": "^3.0.6",
-    "@types/react-dom": "^18.2.6",
+    "@types/react-dom": "^18.2.7",
     "@types/react-responsive": "^8.0.5",
     "@types/react-transition-group": "^4.4.6",
-    "@typescript-eslint/eslint-plugin": "^5.61.0",
-    "@typescript-eslint/parser": "^5.61.0",
-    "buffer": "^6.0.0",
-    "eslint": "^8.44.0",
-    "eslint-config-prettier": "^8.8.0",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-react": "^7.32.2",
+    "@typescript-eslint/eslint-plugin": "^6.7.2",
+    "@typescript-eslint/parser": "^6.7.2",
+    "buffer": "^6.0.3",
+    "eslint": "^8.49.0",
+    "eslint-config-prettier": "^9.0.0",
+    "eslint-plugin-prettier": "^5.0.0",
+    "eslint-plugin-react": "^7.33.2",
     "eslint-plugin-react-hooks": "^4.6.0",
-    "parcel": "latest",
-    "prettier": "^3.0.0",
+    "parcel": "^2.9.3",
+    "prettier": "^3.0.3",
     "process": "^0.11.10",
     "ts-node": "^10.9.1",
-    "typescript": "^5.1.6",
+    "typescript": "^5.2.2",
     "typescript-language-server": "^3.3.2"
   }
 }
\ No newline at end of file
diff --git a/FrontEnd/pnpm-lock.yaml b/FrontEnd/pnpm-lock.yaml
index 3f62918b..7084e82c 100644
--- a/FrontEnd/pnpm-lock.yaml
+++ b/FrontEnd/pnpm-lock.yaml
@@ -6,20 +6,20 @@ settings:
 
 dependencies:
   '@floating-ui/react-dom':
-    specifier: ^2.0.1
-    version: 2.0.1(react-dom@18.2.0)(react@18.2.0)
+    specifier: ^2.0.2
+    version: 2.0.2(react-dom@18.2.0)(react@18.2.0)
   '@microsoft/signalr':
-    specifier: ^7.0.7
-    version: 7.0.7
+    specifier: ^7.0.11
+    version: 7.0.11
   axios:
-    specifier: ^1.4.0
-    version: 1.4.0
+    specifier: ^1.5.0
+    version: 1.5.0
   bootstrap:
-    specifier: ^5.3.0
-    version: 5.3.0(@popperjs/core@2.11.8)
+    specifier: ^5.3.2
+    version: 5.3.2(@popperjs/core@2.11.8)
   bootstrap-icons:
-    specifier: ^1.10.5
-    version: 1.10.5
+    specifier: ^1.11.1
+    version: 1.11.1
   classnames:
     specifier: ^2.3.2
     version: 2.3.2
@@ -27,11 +27,11 @@ dependencies:
     specifier: ^4.2.3
     version: 4.2.3
   core-js:
-    specifier: ^3.31.1
-    version: 3.31.1
+    specifier: ^3.32.2
+    version: 3.32.2
   i18next:
-    specifier: ^23.2.8
-    version: 23.2.8
+    specifier: ^23.5.1
+    version: 23.5.1
   i18next-browser-languagedetector:
     specifier: ^7.1.0
     version: 7.1.0
@@ -39,8 +39,8 @@ dependencies:
     specifier: ^4.17.21
     version: 4.17.21
   marked:
-    specifier: ^5.1.1
-    version: 5.1.1
+    specifier: ^9.0.3
+    version: 9.0.3
   moment:
     specifier: ^2.29.4
     version: 2.29.4
@@ -51,8 +51,8 @@ dependencies:
     specifier: ^18.2.0
     version: 18.2.0(react@18.2.0)
   react-i18next:
-    specifier: ^13.0.1
-    version: 13.0.1(i18next@23.2.8)(react-dom@18.2.0)(react@18.2.0)
+    specifier: ^13.2.2
+    version: 13.2.2(i18next@23.5.1)(react-dom@18.2.0)(react@18.2.0)
   react-popper:
     specifier: ^2.3.0
     version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
@@ -60,8 +60,8 @@ dependencies:
     specifier: ^9.0.2
     version: 9.0.2(react@18.2.0)
   react-router-dom:
-    specifier: ^6.14.1
-    version: 6.14.1(react-dom@18.2.0)(react@18.2.0)
+    specifier: ^6.16.0
+    version: 6.16.0(react-dom@18.2.0)(react@18.2.0)
   react-transition-group:
     specifier: ^4.4.5
     version: 4.4.5(react-dom@18.2.0)(react@18.2.0)
@@ -80,32 +80,32 @@ devDependencies:
     specifier: 2.9.3
     version: 2.9.3(@parcel/core@2.9.3)
   '@tsconfig/node20':
-    specifier: ^1.0.2
-    version: 1.0.2
+    specifier: ^20.1.2
+    version: 20.1.2
   '@types/color':
-    specifier: ^3.0.3
-    version: 3.0.3
+    specifier: ^3.0.4
+    version: 3.0.4
   '@types/lodash':
-    specifier: ^4.14.195
-    version: 4.14.195
+    specifier: ^4.14.198
+    version: 4.14.198
   '@types/marked':
-    specifier: ^5.0.0
-    version: 5.0.0
+    specifier: ^5.0.1
+    version: 5.0.1
   '@types/node':
-    specifier: ^20.4.1
-    version: 20.4.1
+    specifier: ^20.6.3
+    version: 20.6.3
   '@types/parcel-env':
     specifier: ^0.0.1
     version: 0.0.1
   '@types/react':
-    specifier: ^18.2.14
-    version: 18.2.14
+    specifier: ^18.2.22
+    version: 18.2.22
   '@types/react-color':
     specifier: ^3.0.6
     version: 3.0.6
   '@types/react-dom':
-    specifier: ^18.2.6
-    version: 18.2.6
+    specifier: ^18.2.7
+    version: 18.2.7
   '@types/react-responsive':
     specifier: ^8.0.5
     version: 8.0.5
@@ -113,44 +113,44 @@ devDependencies:
     specifier: ^4.4.6
     version: 4.4.6
   '@typescript-eslint/eslint-plugin':
-    specifier: ^5.61.0
-    version: 5.61.0(@typescript-eslint/parser@5.61.0)(eslint@8.44.0)(typescript@5.1.6)
+    specifier: ^6.7.2
+    version: 6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.49.0)(typescript@5.2.2)
   '@typescript-eslint/parser':
-    specifier: ^5.61.0
-    version: 5.61.0(eslint@8.44.0)(typescript@5.1.6)
+    specifier: ^6.7.2
+    version: 6.7.2(eslint@8.49.0)(typescript@5.2.2)
   buffer:
-    specifier: ^6.0.0
-    version: 6.0.0
+    specifier: ^6.0.3
+    version: 6.0.3
   eslint:
-    specifier: ^8.44.0
-    version: 8.44.0
+    specifier: ^8.49.0
+    version: 8.49.0
   eslint-config-prettier:
-    specifier: ^8.8.0
-    version: 8.8.0(eslint@8.44.0)
+    specifier: ^9.0.0
+    version: 9.0.0(eslint@8.49.0)
   eslint-plugin-prettier:
-    specifier: ^4.2.1
-    version: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.44.0)(prettier@3.0.0)
+    specifier: ^5.0.0
+    version: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3)
   eslint-plugin-react:
-    specifier: ^7.32.2
-    version: 7.32.2(eslint@8.44.0)
+    specifier: ^7.33.2
+    version: 7.33.2(eslint@8.49.0)
   eslint-plugin-react-hooks:
     specifier: ^4.6.0
-    version: 4.6.0(eslint@8.44.0)
+    version: 4.6.0(eslint@8.49.0)
   parcel:
-    specifier: latest
-    version: 2.9.3
+    specifier: ^2.9.3
+    version: 2.9.3(typescript@5.2.2)
   prettier:
-    specifier: ^3.0.0
-    version: 3.0.0
+    specifier: ^3.0.3
+    version: 3.0.3
   process:
     specifier: ^0.11.10
     version: 0.11.10
   ts-node:
     specifier: ^10.9.1
-    version: 10.9.1(@types/node@20.4.1)(typescript@5.1.6)
+    version: 10.9.1(@types/node@20.6.3)(typescript@5.2.2)
   typescript:
-    specifier: ^5.1.6
-    version: 5.1.6
+    specifier: ^5.2.2
+    version: 5.2.2
   typescript-language-server:
     specifier: ^3.3.2
     version: 3.3.2
@@ -162,40 +162,41 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /@babel/code-frame@7.22.5:
-    resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
+  /@babel/code-frame@7.22.13:
+    resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/highlight': 7.22.5
+      '@babel/highlight': 7.22.20
+      chalk: 2.4.2
     dev: true
 
-  /@babel/helper-validator-identifier@7.22.5:
-    resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
+  /@babel/helper-validator-identifier@7.22.20:
+    resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
     engines: {node: '>=6.9.0'}
     dev: true
 
-  /@babel/highlight@7.22.5:
-    resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
+  /@babel/highlight@7.22.20:
+    resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/helper-validator-identifier': 7.22.5
+      '@babel/helper-validator-identifier': 7.22.20
       chalk: 2.4.2
       js-tokens: 4.0.0
     dev: true
 
-  /@babel/runtime-corejs3@7.22.6:
-    resolution: {integrity: sha512-M+37LLIRBTEVjktoJjbw4KVhupF0U/3PYUCbBwgAd9k17hoKhRu1n935QiG7Tuxv0LJOMrb2vuKEeYUlv0iyiw==}
+  /@babel/runtime-corejs3@7.22.15:
+    resolution: {integrity: sha512-SAj8oKi8UogVi6eXQXKNPu8qZ78Yzy7zawrlTr0M+IuW/g8Qe9gVDhGcF9h1S69OyACpYoLxEzpjs1M15sI5wQ==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      core-js-pure: 3.31.1
-      regenerator-runtime: 0.13.11
+      core-js-pure: 3.32.2
+      regenerator-runtime: 0.14.0
     dev: false
 
-  /@babel/runtime@7.22.6:
-    resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
+  /@babel/runtime@7.22.15:
+    resolution: {integrity: sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      regenerator-runtime: 0.13.11
+      regenerator-runtime: 0.14.0
     dev: false
 
   /@cspotcode/source-map-support@0.8.1:
@@ -205,29 +206,29 @@ packages:
       '@jridgewell/trace-mapping': 0.3.9
     dev: true
 
-  /@eslint-community/eslint-utils@4.4.0(eslint@8.44.0):
+  /@eslint-community/eslint-utils@4.4.0(eslint@8.49.0):
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
     dependencies:
-      eslint: 8.44.0
-      eslint-visitor-keys: 3.4.1
+      eslint: 8.49.0
+      eslint-visitor-keys: 3.4.3
     dev: true
 
-  /@eslint-community/regexpp@4.5.1:
-    resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==}
+  /@eslint-community/regexpp@4.8.1:
+    resolution: {integrity: sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==}
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
     dev: true
 
-  /@eslint/eslintrc@2.1.0:
-    resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==}
+  /@eslint/eslintrc@2.1.2:
+    resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
       ajv: 6.12.6
       debug: 4.3.4
-      espree: 9.6.0
-      globals: 13.20.0
+      espree: 9.6.1
+      globals: 13.21.0
       ignore: 5.2.4
       import-fresh: 3.3.0
       js-yaml: 4.1.0
@@ -237,41 +238,41 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint/js@8.44.0:
-    resolution: {integrity: sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==}
+  /@eslint/js@8.49.0:
+    resolution: {integrity: sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
-  /@floating-ui/core@1.4.1:
-    resolution: {integrity: sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==}
+  /@floating-ui/core@1.5.0:
+    resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==}
     dependencies:
-      '@floating-ui/utils': 0.1.1
+      '@floating-ui/utils': 0.1.4
     dev: false
 
-  /@floating-ui/dom@1.5.1:
-    resolution: {integrity: sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==}
+  /@floating-ui/dom@1.5.3:
+    resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==}
     dependencies:
-      '@floating-ui/core': 1.4.1
-      '@floating-ui/utils': 0.1.1
+      '@floating-ui/core': 1.5.0
+      '@floating-ui/utils': 0.1.4
     dev: false
 
-  /@floating-ui/react-dom@2.0.1(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==}
+  /@floating-ui/react-dom@2.0.2(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==}
     peerDependencies:
       react: '>=16.8.0'
       react-dom: '>=16.8.0'
     dependencies:
-      '@floating-ui/dom': 1.5.1
+      '@floating-ui/dom': 1.5.3
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: false
 
-  /@floating-ui/utils@0.1.1:
-    resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
+  /@floating-ui/utils@0.1.4:
+    resolution: {integrity: sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==}
     dev: false
 
-  /@humanwhocodes/config-array@0.11.10:
-    resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
+  /@humanwhocodes/config-array@0.11.11:
+    resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
     engines: {node: '>=10.10.0'}
     dependencies:
       '@humanwhocodes/object-schema': 1.2.1
@@ -364,13 +365,13 @@ packages:
     dev: true
     optional: true
 
-  /@microsoft/signalr@7.0.7:
-    resolution: {integrity: sha512-RMWZLTxnjWPSaS9PYZxXAttql2JDM/+IsSJk0nACFhpLjnSw8UWfvUxOv/QjZSqLxhuksXxzBJ/91xUP6Y7Nvg==}
+  /@microsoft/signalr@7.0.11:
+    resolution: {integrity: sha512-//6ipnYKhHf2MJgM+MQSlgB5L/pcYeZ+v4w6YAr4epRM1iSDQ6WjUkCVX2ZMxcY06XGlLzggs3Z9ZIcL9ws9KQ==}
     dependencies:
       abort-controller: 3.0.0
       eventsource: 2.0.2
       fetch-cookie: 2.1.0
-      node-fetch: 2.6.12
+      node-fetch: 2.7.0
       ws: 7.5.9
     transitivePeerDependencies:
       - bufferutil
@@ -499,7 +500,7 @@ packages:
       - '@parcel/core'
     dev: true
 
-  /@parcel/config-default@2.9.3(@parcel/core@2.9.3):
+  /@parcel/config-default@2.9.3(@parcel/core@2.9.3)(typescript@5.2.2):
     resolution: {integrity: sha512-tqN5tF7QnVABDZAu76co5E6N8mA9n8bxiWdK4xYyINYFIEHgX172oRTqXTnhEMjlMrdmASxvnGlbaPBaVnrCTw==}
     peerDependencies:
       '@parcel/core': ^2.9.3
@@ -509,7 +510,7 @@ packages:
       '@parcel/core': 2.9.3
       '@parcel/namer-default': 2.9.3(@parcel/core@2.9.3)
       '@parcel/optimizer-css': 2.9.3(@parcel/core@2.9.3)
-      '@parcel/optimizer-htmlnano': 2.9.3(@parcel/core@2.9.3)
+      '@parcel/optimizer-htmlnano': 2.9.3(@parcel/core@2.9.3)(typescript@5.2.2)
       '@parcel/optimizer-image': 2.9.3(@parcel/core@2.9.3)
       '@parcel/optimizer-svgo': 2.9.3(@parcel/core@2.9.3)
       '@parcel/optimizer-swc': 2.9.3(@parcel/core@2.9.3)
@@ -543,6 +544,7 @@ packages:
       - relateurl
       - srcset
       - terser
+      - typescript
       - uncss
     dev: true
 
@@ -567,12 +569,12 @@ packages:
       '@parcel/workers': 2.9.3(@parcel/core@2.9.3)
       abortcontroller-polyfill: 1.7.5
       base-x: 3.0.9
-      browserslist: 4.21.9
+      browserslist: 4.21.10
       clone: 2.1.2
       dotenv: 7.0.0
       dotenv-expand: 5.1.0
       json5: 2.2.3
-      msgpackr: 1.9.5
+      msgpackr: 1.9.9
       nullthrows: 1.1.1
       semver: 7.5.4
     dev: true
@@ -605,7 +607,7 @@ packages:
       '@parcel/fs-search': 2.9.3
       '@parcel/types': 2.9.3(@parcel/core@2.9.3)
       '@parcel/utils': 2.9.3
-      '@parcel/watcher': 2.2.0
+      '@parcel/watcher': 2.3.0
       '@parcel/workers': 2.9.3(@parcel/core@2.9.3)
     dev: true
 
@@ -671,19 +673,19 @@ packages:
       '@parcel/plugin': 2.9.3(@parcel/core@2.9.3)
       '@parcel/source-map': 2.1.1
       '@parcel/utils': 2.9.3
-      browserslist: 4.21.9
-      lightningcss: 1.21.5
+      browserslist: 4.21.10
+      lightningcss: 1.22.0
       nullthrows: 1.1.1
     transitivePeerDependencies:
       - '@parcel/core'
     dev: true
 
-  /@parcel/optimizer-htmlnano@2.9.3(@parcel/core@2.9.3):
+  /@parcel/optimizer-htmlnano@2.9.3(@parcel/core@2.9.3)(typescript@5.2.2):
     resolution: {integrity: sha512-9g/KBck3c6DokmJfvJ5zpHFBiCSolaGrcsTGx8C3YPdCTVTI9P1TDCwUxvAr4LjpcIRSa82wlLCI+nF6sSgxKA==}
     engines: {node: '>= 12.0.0', parcel: ^2.9.3}
     dependencies:
       '@parcel/plugin': 2.9.3(@parcel/core@2.9.3)
-      htmlnano: 2.0.4(svgo@2.8.0)
+      htmlnano: 2.0.4(svgo@2.8.0)(typescript@5.2.2)
       nullthrows: 1.1.1
       posthtml: 0.16.6
       svgo: 2.8.0
@@ -695,6 +697,7 @@ packages:
       - relateurl
       - srcset
       - terser
+      - typescript
       - uncss
     dev: true
 
@@ -731,7 +734,7 @@ packages:
       '@parcel/plugin': 2.9.3(@parcel/core@2.9.3)
       '@parcel/source-map': 2.1.1
       '@parcel/utils': 2.9.3
-      '@swc/core': 1.3.68
+      '@swc/core': 1.3.86
       nullthrows: 1.1.1
     transitivePeerDependencies:
       - '@parcel/core'
@@ -790,7 +793,7 @@ packages:
       '@parcel/plugin': 2.9.3(@parcel/core@2.9.3)
       '@parcel/source-map': 2.1.1
       '@parcel/utils': 2.9.3
-      globals: 13.20.0
+      globals: 13.21.0
       nullthrows: 1.1.1
     transitivePeerDependencies:
       - '@parcel/core'
@@ -950,7 +953,7 @@ packages:
       '@parcel/plugin': 2.9.3(@parcel/core@2.9.3)
       '@parcel/source-map': 2.1.1
       '@parcel/utils': 2.9.3
-      browserslist: 4.21.9
+      browserslist: 4.21.10
       json5: 2.2.3
       nullthrows: 1.1.1
       semver: 7.5.4
@@ -966,8 +969,8 @@ packages:
       '@parcel/plugin': 2.9.3(@parcel/core@2.9.3)
       '@parcel/source-map': 2.1.1
       '@parcel/utils': 2.9.3
-      browserslist: 4.21.9
-      lightningcss: 1.21.5
+      browserslist: 4.21.10
+      lightningcss: 1.22.0
       nullthrows: 1.1.1
     transitivePeerDependencies:
       - '@parcel/core'
@@ -1015,8 +1018,8 @@ packages:
       '@parcel/source-map': 2.1.1
       '@parcel/utils': 2.9.3
       '@parcel/workers': 2.9.3(@parcel/core@2.9.3)
-      '@swc/helpers': 0.5.1
-      browserslist: 4.21.9
+      '@swc/helpers': 0.5.2
+      browserslist: 4.21.10
       nullthrows: 1.1.1
       regenerator-runtime: 0.13.11
       semver: 7.5.4
@@ -1139,8 +1142,8 @@ packages:
       nullthrows: 1.1.1
     dev: true
 
-  /@parcel/watcher-android-arm64@2.2.0:
-    resolution: {integrity: sha512-nU2wh00CTQT9rr1TIKTjdQ9lAGYpmz6XuKw0nAwAN+S2A5YiD55BK1u+E5WMCT8YOIDe/n6gaj4o/Bi9294SSQ==}
+  /@parcel/watcher-android-arm64@2.3.0:
+    resolution: {integrity: sha512-f4o9eA3dgk0XRT3XhB0UWpWpLnKgrh1IwNJKJ7UJek7eTYccQ8LR7XUWFKqw6aEq5KUNlCcGvSzKqSX/vtWVVA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [android]
@@ -1148,8 +1151,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-darwin-arm64@2.2.0:
-    resolution: {integrity: sha512-cJl0UZDcodciy3TDMomoK/Huxpjlkkim3SyMgWzjovHGOZKNce9guLz2dzuFwfObBFCjfznbFMIvAZ5syXotYw==}
+  /@parcel/watcher-darwin-arm64@2.3.0:
+    resolution: {integrity: sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [darwin]
@@ -1157,8 +1160,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-darwin-x64@2.2.0:
-    resolution: {integrity: sha512-QI77zxaGrCV1StKcoRYfsUfmUmvPMPfQrubkBBy5XujV2fwaLgZivQOTQMBgp5K2+E19u1ufpspKXAPqSzpbyg==}
+  /@parcel/watcher-darwin-x64@2.3.0:
+    resolution: {integrity: sha512-20oBj8LcEOnLE3mgpy6zuOq8AplPu9NcSSSfyVKgfOhNAc4eF4ob3ldj0xWjGGbOF7Dcy1Tvm6ytvgdjlfUeow==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [darwin]
@@ -1166,8 +1169,17 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-linux-arm-glibc@2.2.0:
-    resolution: {integrity: sha512-I2GPBcAXazPzabCmfsa3HRRW+MGlqxYd8g8RIueJU+a4o5nyNZDz0CR1cu0INT0QSQXEZV7w6UE8Hz9CF8u3Pg==}
+  /@parcel/watcher-freebsd-x64@2.3.0:
+    resolution: {integrity: sha512-7LftKlaHunueAEiojhCn+Ef2CTXWsLgTl4hq0pkhkTBFI3ssj2bJXmH2L67mKpiAD5dz66JYk4zS66qzdnIOgw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-linux-arm-glibc@2.3.0:
+    resolution: {integrity: sha512-1apPw5cD2xBv1XIHPUlq0cO6iAaEUQ3BcY0ysSyD9Kuyw4MoWm1DV+W9mneWI+1g6OeP6dhikiFE6BlU+AToTQ==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
@@ -1175,8 +1187,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-linux-arm64-glibc@2.2.0:
-    resolution: {integrity: sha512-St5mlfp+2lS9AmgixUqfwJa/DwVmTCJxC1HcOubUTz6YFOKIlkHCeUa1Bxi4E/tR/HSez8+heXHL8HQkJ4Bd8g==}
+  /@parcel/watcher-linux-arm64-glibc@2.3.0:
+    resolution: {integrity: sha512-mQ0gBSQEiq1k/MMkgcSB0Ic47UORZBmWoAWlMrTW6nbAGoLZP+h7AtUM7H3oDu34TBFFvjy4JCGP43JlylkTQA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
@@ -1184,8 +1196,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-linux-arm64-musl@2.2.0:
-    resolution: {integrity: sha512-jS+qfhhoOBVWwMLP65MaG8xdInMK30pPW8wqTCg2AAuVJh5xepMbzkhHJ4zURqHiyY3EiIRuYu4ONJKCxt8iqA==}
+  /@parcel/watcher-linux-arm64-musl@2.3.0:
+    resolution: {integrity: sha512-LXZAExpepJew0Gp8ZkJ+xDZaTQjLHv48h0p0Vw2VMFQ8A+RKrAvpFuPVCVwKJCr5SE+zvaG+Etg56qXvTDIedw==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
@@ -1193,8 +1205,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-linux-x64-glibc@2.2.0:
-    resolution: {integrity: sha512-xJvJ7R2wJdi47WZBFS691RDOWvP1j/IAs3EXaWVhDI8FFITbWrWaln7KoNcR0Y3T+ZwimFY/cfb0PNht1q895g==}
+  /@parcel/watcher-linux-x64-glibc@2.3.0:
+    resolution: {integrity: sha512-P7Wo91lKSeSgMTtG7CnBS6WrA5otr1K7shhSjKHNePVmfBHDoAOHYRXgUmhiNfbcGk0uMCHVcdbfxtuiZCHVow==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
@@ -1202,8 +1214,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-linux-x64-musl@2.2.0:
-    resolution: {integrity: sha512-D+NMpgr23a+RI5mu8ZPKWy7AqjBOkURFDgP5iIXXEf/K3hm0jJ3ogzi0Ed2237B/CdYREimCgXyeiAlE/FtwyA==}
+  /@parcel/watcher-linux-x64-musl@2.3.0:
+    resolution: {integrity: sha512-+kiRE1JIq8QdxzwoYY+wzBs9YbJ34guBweTK8nlzLKimn5EQ2b2FSC+tAOpq302BuIMjyuUGvBiUhEcLIGMQ5g==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
@@ -1211,8 +1223,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-win32-arm64@2.2.0:
-    resolution: {integrity: sha512-z225cPn3aygJsyVUOWwfyW+fY0Tvk7N3XCOl66qUPFxpbuXeZuiuuJemmtm8vxyqa3Ur7peU/qJxrpC64aeI7Q==}
+  /@parcel/watcher-win32-arm64@2.3.0:
+    resolution: {integrity: sha512-35gXCnaz1AqIXpG42evcoP2+sNL62gZTMZne3IackM+6QlfMcJLy3DrjuL6Iks7Czpd3j4xRBzez3ADCj1l7Aw==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [win32]
@@ -1220,8 +1232,17 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher-win32-x64@2.2.0:
-    resolution: {integrity: sha512-JqGW0RJ61BkKx+yYzIURt9s53P7xMVbv0uxYPzAXLBINGaFmkIKSuUPyBVfy8TMbvp93lvF4SPBNDzVRJfvgOw==}
+  /@parcel/watcher-win32-ia32@2.3.0:
+    resolution: {integrity: sha512-FJS/IBQHhRpZ6PiCjFt1UAcPr0YmCLHRbTc00IBTrelEjlmmgIVLeOx4MSXzx2HFEy5Jo5YdhGpxCuqCyDJ5ow==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@parcel/watcher-win32-x64@2.3.0:
+    resolution: {integrity: sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [win32]
@@ -1229,8 +1250,8 @@ packages:
     dev: true
     optional: true
 
-  /@parcel/watcher@2.2.0:
-    resolution: {integrity: sha512-71S4TF+IMyAn24PK4KSkdKtqJDR3zRzb0HE3yXpacItqTM7XfF2f5q9NEGLEVl0dAaBAGfNwDCjH120y25F6Tg==}
+  /@parcel/watcher@2.3.0:
+    resolution: {integrity: sha512-pW7QaFiL11O0BphO+bq3MgqeX/INAk9jgBldVDYjlQPO4VddoZnF22TcF9onMhnLVHuNqBJeRf+Fj7eezi/+rQ==}
     engines: {node: '>= 10.0.0'}
     dependencies:
       detect-libc: 1.0.3
@@ -1238,16 +1259,18 @@ packages:
       micromatch: 4.0.5
       node-addon-api: 7.0.0
     optionalDependencies:
-      '@parcel/watcher-android-arm64': 2.2.0
-      '@parcel/watcher-darwin-arm64': 2.2.0
-      '@parcel/watcher-darwin-x64': 2.2.0
-      '@parcel/watcher-linux-arm-glibc': 2.2.0
-      '@parcel/watcher-linux-arm64-glibc': 2.2.0
-      '@parcel/watcher-linux-arm64-musl': 2.2.0
-      '@parcel/watcher-linux-x64-glibc': 2.2.0
-      '@parcel/watcher-linux-x64-musl': 2.2.0
-      '@parcel/watcher-win32-arm64': 2.2.0
-      '@parcel/watcher-win32-x64': 2.2.0
+      '@parcel/watcher-android-arm64': 2.3.0
+      '@parcel/watcher-darwin-arm64': 2.3.0
+      '@parcel/watcher-darwin-x64': 2.3.0
+      '@parcel/watcher-freebsd-x64': 2.3.0
+      '@parcel/watcher-linux-arm-glibc': 2.3.0
+      '@parcel/watcher-linux-arm64-glibc': 2.3.0
+      '@parcel/watcher-linux-arm64-musl': 2.3.0
+      '@parcel/watcher-linux-x64-glibc': 2.3.0
+      '@parcel/watcher-linux-x64-musl': 2.3.0
+      '@parcel/watcher-win32-arm64': 2.3.0
+      '@parcel/watcher-win32-ia32': 2.3.0
+      '@parcel/watcher-win32-x64': 2.3.0
     dev: true
 
   /@parcel/workers@2.9.3(@parcel/core@2.9.3):
@@ -1265,17 +1288,29 @@ packages:
       nullthrows: 1.1.1
     dev: true
 
+  /@pkgr/utils@2.4.2:
+    resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==}
+    engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+    dependencies:
+      cross-spawn: 7.0.3
+      fast-glob: 3.3.1
+      is-glob: 4.0.3
+      open: 9.1.0
+      picocolors: 1.0.0
+      tslib: 2.6.2
+    dev: true
+
   /@popperjs/core@2.11.8:
     resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
     dev: false
 
-  /@remix-run/router@1.7.1:
-    resolution: {integrity: sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==}
-    engines: {node: '>=14'}
+  /@remix-run/router@1.9.0:
+    resolution: {integrity: sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==}
+    engines: {node: '>=14.0.0'}
     dev: false
 
-  /@swc/core-darwin-arm64@1.3.68:
-    resolution: {integrity: sha512-Z5pNxeuP2NxpOHTzDQkJs0wAPLnTlglZnR3WjObijwvdwT/kw1Y5EPDKM/BVSIeG40SPMkDLBbI0aj0qyXzrBA==}
+  /@swc/core-darwin-arm64@1.3.86:
+    resolution: {integrity: sha512-hMvSDms0sJJHNtRa3Vhmr9StWN1vmikbf5VE0IZUYGnF1/JZTkXU1h6CdNUY4Hr6i7uCZjH6BEhxFHX1JtKV4w==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [darwin]
@@ -1283,8 +1318,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-darwin-x64@1.3.68:
-    resolution: {integrity: sha512-ZHl42g6yXhfX4PzAQ0BNvBXpt/OcbAHfubWRN6eXELK3fiNnxL7QBW1if7iizlq6iA+Mj1pwHyyUit1pz0+fgA==}
+  /@swc/core-darwin-x64@1.3.86:
+    resolution: {integrity: sha512-Jro6HVH4uSOBM7tTDaQNKLNc8BJV7n+SO+Ft2HAZINyeKJS/8MfEYneG7Vmqg18gv00c6dz9AOCcyz+BR7BFkQ==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [darwin]
@@ -1292,8 +1327,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-linux-arm-gnueabihf@1.3.68:
-    resolution: {integrity: sha512-Mk8f6KCOQ2CNAR4PtWajIjS6XKSSR7ZYDOCf1GXRxhS3qEyQH7V8elWvqWYqHcT4foO60NUmxA/NOM/dQrdO1A==}
+  /@swc/core-linux-arm-gnueabihf@1.3.86:
+    resolution: {integrity: sha512-wYB9m0pzXJVSzedXSl4JwS3gKtvcPinpe9MbkddezpqL7OjyDP6pHHW9qIucsfgCrtMtbPC2nqulXLPtAAyIjw==}
     engines: {node: '>=10'}
     cpu: [arm]
     os: [linux]
@@ -1301,8 +1336,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-linux-arm64-gnu@1.3.68:
-    resolution: {integrity: sha512-RhBllggh9t9sIxaRgRcGrVaS7fDk6KsIqR6b9+dwU5OyDr4ZyHWw1ZaH/1/HAebuXYhNBjoNUiRtca6lKRIPgQ==}
+  /@swc/core-linux-arm64-gnu@1.3.86:
+    resolution: {integrity: sha512-fR44IyK5cdCaO8cC++IEH0Jn03tWnunJnjzA99LxlE5TRInSIOvFm+g5OSUQZDAvEXmQ38sd31LO2HOoDS1Edw==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
@@ -1310,8 +1345,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-linux-arm64-musl@1.3.68:
-    resolution: {integrity: sha512-8K3zjU+tFgn6yGDEeD343gkKaHU9dhz77NiVkI1VzwRaT/Ag5pwl5eMQ1yStm8koNFzn3zq6rGjHfI5g2yI5Wg==}
+  /@swc/core-linux-arm64-musl@1.3.86:
+    resolution: {integrity: sha512-EUPfdbK4dUk/nkX3Vmv/47XH+DqHOa9JI0CTthvJ8/ZXei1MKDUsUc+tI1zMQX2uCuSkSWsEIEpCmA0tMwFhtw==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
@@ -1319,8 +1354,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-linux-x64-gnu@1.3.68:
-    resolution: {integrity: sha512-4xAnvsBOyeTL0AB8GWlRKDM/hsysJ5jr5qvdKKI3rZfJgnnxl/xSX6TJKPsJ8gygfUJ3BmfCbmUmEyeDZ3YPvA==}
+  /@swc/core-linux-x64-gnu@1.3.86:
+    resolution: {integrity: sha512-snVZZWv8XgNVaKrTxtO3rUN+BbbB6I8Fqwe8zM/DWGJ096J13r89doQ48x5ZyO+bW4D48eZIWP5pdfSW7oBE3w==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
@@ -1328,8 +1363,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-linux-x64-musl@1.3.68:
-    resolution: {integrity: sha512-RCpaBo1fcpy1EFdjF+I7N4lfzOaHXVV0iMw/ABM+0PD6tp3V/9pxsguaZyeAHyEiUlDA6PZ4TfXv5zfnXEgW4Q==}
+  /@swc/core-linux-x64-musl@1.3.86:
+    resolution: {integrity: sha512-PnnksUJymEJkdnbV2orOSOSB441UqsxYbJge9zbr5UTRXUfWO3eFRV0iTBegjTlOQGbW6yN+YRSDkenTbmCI6g==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
@@ -1337,8 +1372,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-win32-arm64-msvc@1.3.68:
-    resolution: {integrity: sha512-v2WZvXrSslYEpY1nqpItyamL4DyaJinmOkXvM8Bc1LLKU5rGuvmBdjUYg/5Y+o0AUynuiWubpgHNOkBWiCvfqw==}
+  /@swc/core-win32-arm64-msvc@1.3.86:
+    resolution: {integrity: sha512-XlGEGyHwLndm08VvgeAPGj40L+Hx575MQC+2fsyB1uSNUN+uf7fvke+wc7k50a92CaQe/8foLyIR5faayozEJA==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [win32]
@@ -1346,8 +1381,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-win32-ia32-msvc@1.3.68:
-    resolution: {integrity: sha512-HH5NJrIdzkJs+1xxprie0qSCMBeL9yeEhcC1yZTzYv8bwmabOUSdtKIqS55iYP/2hLWn9CTbvKPmLOIhCopW3Q==}
+  /@swc/core-win32-ia32-msvc@1.3.86:
+    resolution: {integrity: sha512-U1BhZa1x9yn+wZGTQmt1cYR79a0FzW/wL6Jas1Pn0bykKLxdRU4mCeZt2P+T3buLm8jr8LpPWiCrbvr658PzwA==}
     engines: {node: '>=10'}
     cpu: [ia32]
     os: [win32]
@@ -1355,8 +1390,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core-win32-x64-msvc@1.3.68:
-    resolution: {integrity: sha512-9HZVtLQUgK8r/yXQdwe0VBexbIcrY6+fBROhs7AAPWdewpaUeLkwQEJk6TbYr9CQuHw26FFGg6SjwAiqXF+kgQ==}
+  /@swc/core-win32-x64-msvc@1.3.86:
+    resolution: {integrity: sha512-wRoQUajqpE3wITHhZVj/6BPu/QwHriFHLHuJA+9y6PeGtUtTmntL42aBKXIFhfL767dYFtohyNg1uZ9eqbGyGQ==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [win32]
@@ -1364,8 +1399,8 @@ packages:
     dev: true
     optional: true
 
-  /@swc/core@1.3.68:
-    resolution: {integrity: sha512-njGQuJO+Wy06dEayt70cf0c/KI3HGjm4iW9LLViVLBuYNzJ4SSdNfzejludzufu6im+dsDJ0i3QjgWhAIcVHMQ==}
+  /@swc/core@1.3.86:
+    resolution: {integrity: sha512-bEXUtm37bcmJ3q+geG7Zy4rJNUzpxalXQUrrqX1ZoGj3HRtzdeVZ0L/um3fG2j16qe61t8TX/OIZ2G6j6dkG/w==}
     engines: {node: '>=10'}
     requiresBuild: true
     peerDependencies:
@@ -1373,23 +1408,29 @@ packages:
     peerDependenciesMeta:
       '@swc/helpers':
         optional: true
+    dependencies:
+      '@swc/types': 0.1.5
     optionalDependencies:
-      '@swc/core-darwin-arm64': 1.3.68
-      '@swc/core-darwin-x64': 1.3.68
-      '@swc/core-linux-arm-gnueabihf': 1.3.68
-      '@swc/core-linux-arm64-gnu': 1.3.68
-      '@swc/core-linux-arm64-musl': 1.3.68
-      '@swc/core-linux-x64-gnu': 1.3.68
-      '@swc/core-linux-x64-musl': 1.3.68
-      '@swc/core-win32-arm64-msvc': 1.3.68
-      '@swc/core-win32-ia32-msvc': 1.3.68
-      '@swc/core-win32-x64-msvc': 1.3.68
+      '@swc/core-darwin-arm64': 1.3.86
+      '@swc/core-darwin-x64': 1.3.86
+      '@swc/core-linux-arm-gnueabihf': 1.3.86
+      '@swc/core-linux-arm64-gnu': 1.3.86
+      '@swc/core-linux-arm64-musl': 1.3.86
+      '@swc/core-linux-x64-gnu': 1.3.86
+      '@swc/core-linux-x64-musl': 1.3.86
+      '@swc/core-win32-arm64-msvc': 1.3.86
+      '@swc/core-win32-ia32-msvc': 1.3.86
+      '@swc/core-win32-x64-msvc': 1.3.86
     dev: true
 
-  /@swc/helpers@0.5.1:
-    resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
+  /@swc/helpers@0.5.2:
+    resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
     dependencies:
-      tslib: 2.6.0
+      tslib: 2.6.2
+    dev: true
+
+  /@swc/types@0.1.5:
+    resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
     dev: true
 
   /@trysound/sax@0.2.0:
@@ -1413,12 +1454,12 @@ packages:
     resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
     dev: true
 
-  /@tsconfig/node20@1.0.2:
-    resolution: {integrity: sha512-pw0MmECiSTbBfIlT0x3iQLuJ8s3i2mwYoGxJ3vzqTNMdc4nO2VeqfCOQ/doGFa8iyPlqmBd98/5pBctWz7uN2A==}
+  /@tsconfig/node20@20.1.2:
+    resolution: {integrity: sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==}
     dev: true
 
-  /@types/color-convert@2.0.0:
-    resolution: {integrity: sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==}
+  /@types/color-convert@2.0.1:
+    resolution: {integrity: sha512-GwXanrvq/tBHJtudbl1lSy9Ybt7KS9+rA+YY3bcuIIM+d6jSHUr+5yjO83gtiRpuaPiBccwFjSnAK2qSrIPA7w==}
     dependencies:
       '@types/color-name': 1.1.1
     dev: true
@@ -1427,65 +1468,65 @@ packages:
     resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
     dev: true
 
-  /@types/color@3.0.3:
-    resolution: {integrity: sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==}
+  /@types/color@3.0.4:
+    resolution: {integrity: sha512-OpisS4bqJJwbkkQRrMvURf3DOxBoAg9mysHYI7WgrWpSYHqHGKYBULHdz4ih77SILcLDo/zyHGFyfIl9yb8NZQ==}
     dependencies:
-      '@types/color-convert': 2.0.0
+      '@types/color-convert': 2.0.1
     dev: true
 
-  /@types/json-schema@7.0.12:
-    resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
+  /@types/json-schema@7.0.13:
+    resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==}
     dev: true
 
-  /@types/lodash@4.14.195:
-    resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==}
+  /@types/lodash@4.14.198:
+    resolution: {integrity: sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==}
     dev: true
 
-  /@types/marked@5.0.0:
-    resolution: {integrity: sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==}
+  /@types/marked@5.0.1:
+    resolution: {integrity: sha512-Y3pAUzHKh605fN6fvASsz5FDSWbZcs/65Q6xYRmnIP9ZIYz27T4IOmXfH9gWJV1dpi7f1e7z7nBGUTx/a0ptpA==}
     dev: true
 
-  /@types/node@20.4.1:
-    resolution: {integrity: sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==}
+  /@types/node@20.6.3:
+    resolution: {integrity: sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==}
     dev: true
 
   /@types/parcel-env@0.0.1:
     resolution: {integrity: sha512-8WmdiJ1uEBcW6AOWzQH7i0141ZXZr7B03YfTpguUDrTHXJHwYU9eEOckBRCZzYGrzb4pdoyBlaIMiTee04uqPQ==}
     dev: true
 
-  /@types/prop-types@15.7.5:
-    resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
+  /@types/prop-types@15.7.6:
+    resolution: {integrity: sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg==}
     dev: true
 
   /@types/react-color@3.0.6:
     resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==}
     dependencies:
-      '@types/react': 18.2.14
+      '@types/react': 18.2.22
       '@types/reactcss': 1.2.6
     dev: true
 
-  /@types/react-dom@18.2.6:
-    resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==}
+  /@types/react-dom@18.2.7:
+    resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==}
     dependencies:
-      '@types/react': 18.2.14
+      '@types/react': 18.2.22
     dev: true
 
   /@types/react-responsive@8.0.5:
     resolution: {integrity: sha512-k3gQJgI87oP5IrVZe//3LKJFnAeFaqqWmmtl5eoYL2H3HqFcIhUaE30kRK1CsW3DHdojZxcVj4ZNc2ClsEu2PA==}
     dependencies:
-      '@types/react': 18.2.14
+      '@types/react': 18.2.22
     dev: true
 
   /@types/react-transition-group@4.4.6:
     resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==}
     dependencies:
-      '@types/react': 18.2.14
+      '@types/react': 18.2.22
     dev: true
 
-  /@types/react@18.2.14:
-    resolution: {integrity: sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==}
+  /@types/react@18.2.22:
+    resolution: {integrity: sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==}
     dependencies:
-      '@types/prop-types': 15.7.5
+      '@types/prop-types': 15.7.6
       '@types/scheduler': 0.16.3
       csstype: 3.1.2
     dev: true
@@ -1493,145 +1534,146 @@ packages:
   /@types/reactcss@1.2.6:
     resolution: {integrity: sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==}
     dependencies:
-      '@types/react': 18.2.14
+      '@types/react': 18.2.22
     dev: true
 
   /@types/scheduler@0.16.3:
     resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
     dev: true
 
-  /@types/semver@7.5.0:
-    resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
+  /@types/semver@7.5.2:
+    resolution: {integrity: sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==}
     dev: true
 
-  /@typescript-eslint/eslint-plugin@5.61.0(@typescript-eslint/parser@5.61.0)(eslint@8.44.0)(typescript@5.1.6):
-    resolution: {integrity: sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/eslint-plugin@6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.49.0)(typescript@5.2.2):
+    resolution: {integrity: sha512-ooaHxlmSgZTM6CHYAFRlifqh1OAr3PAQEwi7lhYhaegbnXrnh7CDcHmc3+ihhbQC7H0i4JF0psI5ehzkF6Yl6Q==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
-      '@typescript-eslint/parser': ^5.0.0
-      eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+      '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
+      eslint: ^7.0.0 || ^8.0.0
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@eslint-community/regexpp': 4.5.1
-      '@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@5.1.6)
-      '@typescript-eslint/scope-manager': 5.61.0
-      '@typescript-eslint/type-utils': 5.61.0(eslint@8.44.0)(typescript@5.1.6)
-      '@typescript-eslint/utils': 5.61.0(eslint@8.44.0)(typescript@5.1.6)
+      '@eslint-community/regexpp': 4.8.1
+      '@typescript-eslint/parser': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
+      '@typescript-eslint/scope-manager': 6.7.2
+      '@typescript-eslint/type-utils': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
+      '@typescript-eslint/utils': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
+      '@typescript-eslint/visitor-keys': 6.7.2
       debug: 4.3.4
-      eslint: 8.44.0
+      eslint: 8.49.0
       graphemer: 1.4.0
       ignore: 5.2.4
-      natural-compare-lite: 1.4.0
+      natural-compare: 1.4.0
       semver: 7.5.4
-      tsutils: 3.21.0(typescript@5.1.6)
-      typescript: 5.1.6
+      ts-api-utils: 1.0.3(typescript@5.2.2)
+      typescript: 5.2.2
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@5.61.0(eslint@8.44.0)(typescript@5.1.6):
-    resolution: {integrity: sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/parser@6.7.2(eslint@8.49.0)(typescript@5.2.2):
+    resolution: {integrity: sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
-      eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+      eslint: ^7.0.0 || ^8.0.0
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/scope-manager': 5.61.0
-      '@typescript-eslint/types': 5.61.0
-      '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6)
+      '@typescript-eslint/scope-manager': 6.7.2
+      '@typescript-eslint/types': 6.7.2
+      '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2)
+      '@typescript-eslint/visitor-keys': 6.7.2
       debug: 4.3.4
-      eslint: 8.44.0
-      typescript: 5.1.6
+      eslint: 8.49.0
+      typescript: 5.2.2
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/scope-manager@5.61.0:
-    resolution: {integrity: sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/scope-manager@6.7.2:
+    resolution: {integrity: sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     dependencies:
-      '@typescript-eslint/types': 5.61.0
-      '@typescript-eslint/visitor-keys': 5.61.0
+      '@typescript-eslint/types': 6.7.2
+      '@typescript-eslint/visitor-keys': 6.7.2
     dev: true
 
-  /@typescript-eslint/type-utils@5.61.0(eslint@8.44.0)(typescript@5.1.6):
-    resolution: {integrity: sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/type-utils@6.7.2(eslint@8.49.0)(typescript@5.2.2):
+    resolution: {integrity: sha512-36F4fOYIROYRl0qj95dYKx6kybddLtsbmPIYNK0OBeXv2j9L5nZ17j9jmfy+bIDHKQgn2EZX+cofsqi8NPATBQ==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
-      eslint: '*'
+      eslint: ^7.0.0 || ^8.0.0
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6)
-      '@typescript-eslint/utils': 5.61.0(eslint@8.44.0)(typescript@5.1.6)
+      '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2)
+      '@typescript-eslint/utils': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
       debug: 4.3.4
-      eslint: 8.44.0
-      tsutils: 3.21.0(typescript@5.1.6)
-      typescript: 5.1.6
+      eslint: 8.49.0
+      ts-api-utils: 1.0.3(typescript@5.2.2)
+      typescript: 5.2.2
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/types@5.61.0:
-    resolution: {integrity: sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/types@6.7.2:
+    resolution: {integrity: sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     dev: true
 
-  /@typescript-eslint/typescript-estree@5.61.0(typescript@5.1.6):
-    resolution: {integrity: sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/typescript-estree@6.7.2(typescript@5.2.2):
+    resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@typescript-eslint/types': 5.61.0
-      '@typescript-eslint/visitor-keys': 5.61.0
+      '@typescript-eslint/types': 6.7.2
+      '@typescript-eslint/visitor-keys': 6.7.2
       debug: 4.3.4
       globby: 11.1.0
       is-glob: 4.0.3
       semver: 7.5.4
-      tsutils: 3.21.0(typescript@5.1.6)
-      typescript: 5.1.6
+      ts-api-utils: 1.0.3(typescript@5.2.2)
+      typescript: 5.2.2
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/utils@5.61.0(eslint@8.44.0)(typescript@5.1.6):
-    resolution: {integrity: sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/utils@6.7.2(eslint@8.49.0)(typescript@5.2.2):
+    resolution: {integrity: sha512-ZCcBJug/TS6fXRTsoTkgnsvyWSiXwMNiPzBUani7hDidBdj1779qwM1FIAmpH4lvlOZNF3EScsxxuGifjpLSWQ==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
-      eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
-    dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.44.0)
-      '@types/json-schema': 7.0.12
-      '@types/semver': 7.5.0
-      '@typescript-eslint/scope-manager': 5.61.0
-      '@typescript-eslint/types': 5.61.0
-      '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6)
-      eslint: 8.44.0
-      eslint-scope: 5.1.1
+      eslint: ^7.0.0 || ^8.0.0
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.49.0)
+      '@types/json-schema': 7.0.13
+      '@types/semver': 7.5.2
+      '@typescript-eslint/scope-manager': 6.7.2
+      '@typescript-eslint/types': 6.7.2
+      '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2)
+      eslint: 8.49.0
       semver: 7.5.4
     transitivePeerDependencies:
       - supports-color
       - typescript
     dev: true
 
-  /@typescript-eslint/visitor-keys@5.61.0:
-    resolution: {integrity: sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  /@typescript-eslint/visitor-keys@6.7.2:
+    resolution: {integrity: sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==}
+    engines: {node: ^16.0.0 || >=18.0.0}
     dependencies:
-      '@typescript-eslint/types': 5.61.0
-      eslint-visitor-keys: 3.4.1
+      '@typescript-eslint/types': 6.7.2
+      eslint-visitor-keys: 3.4.3
     dev: true
 
   /abort-controller@3.0.0:
@@ -1707,13 +1749,13 @@ packages:
       is-array-buffer: 3.0.2
     dev: true
 
-  /array-includes@3.1.6:
-    resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==}
+  /array-includes@3.1.7:
+    resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
       get-intrinsic: 1.2.1
       is-string: 1.0.7
     dev: true
@@ -1723,36 +1765,55 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
-  /array.prototype.flat@1.3.1:
-    resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==}
+  /array.prototype.flat@1.3.2:
+    resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
       es-shim-unscopables: 1.0.0
     dev: true
 
-  /array.prototype.flatmap@1.3.1:
-    resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==}
+  /array.prototype.flatmap@1.3.2:
+    resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
       es-shim-unscopables: 1.0.0
     dev: true
 
-  /array.prototype.tosorted@1.1.1:
-    resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==}
+  /array.prototype.tosorted@1.1.2:
+    resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
       es-shim-unscopables: 1.0.0
       get-intrinsic: 1.2.1
     dev: true
 
+  /arraybuffer.prototype.slice@1.0.2:
+    resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      array-buffer-byte-length: 1.0.0
+      call-bind: 1.0.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
+      get-intrinsic: 1.2.1
+      is-array-buffer: 3.0.2
+      is-shared-array-buffer: 1.0.2
+    dev: true
+
+  /asynciterator.prototype@1.0.0:
+    resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==}
+    dependencies:
+      has-symbols: 1.0.3
+    dev: true
+
   /asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
     dev: false
@@ -1762,10 +1823,10 @@ packages:
     engines: {node: '>= 0.4'}
     dev: true
 
-  /axios@1.4.0:
-    resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==}
+  /axios@1.5.0:
+    resolution: {integrity: sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==}
     dependencies:
-      follow-redirects: 1.15.2
+      follow-redirects: 1.15.3
       form-data: 4.0.0
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
@@ -1786,22 +1847,34 @@ packages:
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
     dev: true
 
+  /big-integer@1.6.51:
+    resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
+    engines: {node: '>=0.6'}
+    dev: true
+
   /boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
     dev: true
 
-  /bootstrap-icons@1.10.5:
-    resolution: {integrity: sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==}
+  /bootstrap-icons@1.11.1:
+    resolution: {integrity: sha512-F0DDp7nKUX+x/QtpfRZ+XHFya60ng9nfdpdS59vDDfs4Uhuxp7zym/QavMsu/xx51txkoM9eVmpE7D08N35blw==}
     dev: false
 
-  /bootstrap@5.3.0(@popperjs/core@2.11.8):
-    resolution: {integrity: sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==}
+  /bootstrap@5.3.2(@popperjs/core@2.11.8):
+    resolution: {integrity: sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==}
     peerDependencies:
-      '@popperjs/core': ^2.11.7
+      '@popperjs/core': ^2.11.8
     dependencies:
       '@popperjs/core': 2.11.8
     dev: false
 
+  /bplist-parser@0.2.0:
+    resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==}
+    engines: {node: '>= 5.10.0'}
+    dependencies:
+      big-integer: 1.6.51
+    dev: true
+
   /brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
     dependencies:
@@ -1816,24 +1889,31 @@ packages:
       fill-range: 7.0.1
     dev: true
 
-  /browserslist@4.21.9:
-    resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==}
+  /browserslist@4.21.10:
+    resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     dependencies:
-      caniuse-lite: 1.0.30001513
-      electron-to-chromium: 1.4.454
+      caniuse-lite: 1.0.30001538
+      electron-to-chromium: 1.4.525
       node-releases: 2.0.13
-      update-browserslist-db: 1.0.11(browserslist@4.21.9)
+      update-browserslist-db: 1.0.11(browserslist@4.21.10)
     dev: true
 
-  /buffer@6.0.0:
-    resolution: {integrity: sha512-Sxdxq98A+Y9kRjO/3+mc2IAxIyTAKqzBiYKpeo5EluWnw9535rI4fN8DeMGsiQqpqqaWtFtTdxQgHnku6IEjCA==}
+  /buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
     dependencies:
       base64-js: 1.5.1
       ieee754: 1.2.1
     dev: true
 
+  /bundle-name@3.0.0:
+    resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==}
+    engines: {node: '>=12'}
+    dependencies:
+      run-applescript: 5.0.0
+    dev: true
+
   /call-bind@1.0.2:
     resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
     dependencies:
@@ -1846,8 +1926,8 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
-  /caniuse-lite@1.0.30001513:
-    resolution: {integrity: sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww==}
+  /caniuse-lite@1.0.30001538:
+    resolution: {integrity: sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==}
     dev: true
 
   /chalk@2.4.2:
@@ -1931,24 +2011,30 @@ packages:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
     dev: true
 
-  /core-js-pure@3.31.1:
-    resolution: {integrity: sha512-w+C62kvWti0EPs4KPMCMVv9DriHSXfQOCQ94bGGBiEW5rrbtt/Rz8n5Krhfw9cpFyzXBjf3DB3QnPdEzGDY4Fw==}
+  /core-js-pure@3.32.2:
+    resolution: {integrity: sha512-Y2rxThOuNywTjnX/PgA5vWM6CZ9QB9sz9oGeCixV8MqXZO70z/5SHzf9EeBrEBK0PN36DnEBBu9O/aGWzKuMZQ==}
     requiresBuild: true
     dev: false
 
-  /core-js@3.31.1:
-    resolution: {integrity: sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==}
+  /core-js@3.32.2:
+    resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==}
     requiresBuild: true
     dev: false
 
-  /cosmiconfig@8.2.0:
-    resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==}
+  /cosmiconfig@8.3.6(typescript@5.2.2):
+    resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
     engines: {node: '>=14'}
+    peerDependencies:
+      typescript: '>=4.9.5'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
     dependencies:
       import-fresh: 3.3.0
       js-yaml: 4.1.0
       parse-json: 5.2.0
       path-type: 4.0.0
+      typescript: 5.2.2
     dev: true
 
   /create-require@1.1.1:
@@ -2017,10 +2103,43 @@ packages:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
     dev: true
 
-  /define-properties@1.2.0:
-    resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
+  /default-browser-id@3.0.0:
+    resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
+    engines: {node: '>=12'}
+    dependencies:
+      bplist-parser: 0.2.0
+      untildify: 4.0.0
+    dev: true
+
+  /default-browser@4.0.0:
+    resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==}
+    engines: {node: '>=14.16'}
+    dependencies:
+      bundle-name: 3.0.0
+      default-browser-id: 3.0.0
+      execa: 7.2.0
+      titleize: 3.0.0
+    dev: true
+
+  /define-data-property@1.1.0:
+    resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==}
     engines: {node: '>= 0.4'}
     dependencies:
+      get-intrinsic: 1.2.1
+      gopd: 1.0.1
+      has-property-descriptors: 1.0.0
+    dev: true
+
+  /define-lazy-prop@3.0.0:
+    resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
+    engines: {node: '>=12'}
+    dev: true
+
+  /define-properties@1.2.1:
+    resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      define-data-property: 1.1.0
       has-property-descriptors: 1.0.0
       object-keys: 1.1.1
     dev: true
@@ -2065,7 +2184,7 @@ packages:
   /dom-helpers@5.2.1:
     resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
     dependencies:
-      '@babel/runtime': 7.22.6
+      '@babel/runtime': 7.22.15
       csstype: 3.1.2
     dev: false
 
@@ -2105,8 +2224,8 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
-  /electron-to-chromium@1.4.454:
-    resolution: {integrity: sha512-pmf1rbAStw8UEQ0sr2cdJtWl48ZMuPD9Sto8HVQOq9vx9j2WgDEN6lYoaqFvqEHYOmGA9oRGn7LqWI9ta0YugQ==}
+  /electron-to-chromium@1.4.525:
+    resolution: {integrity: sha512-GIZ620hDK4YmIqAWkscG4W6RwY6gOx1y5J6f4JUQwctiJrqH2oxZYU4mXHi35oV32tr630UcepBzSBGJ/WYcZA==}
     dev: true
 
   /entities@2.2.0:
@@ -2124,16 +2243,17 @@ packages:
       is-arrayish: 0.2.1
     dev: true
 
-  /es-abstract@1.21.2:
-    resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==}
+  /es-abstract@1.22.2:
+    resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==}
     engines: {node: '>= 0.4'}
     dependencies:
       array-buffer-byte-length: 1.0.0
+      arraybuffer.prototype.slice: 1.0.2
       available-typed-arrays: 1.0.5
       call-bind: 1.0.2
       es-set-tostringtag: 2.0.1
       es-to-primitive: 1.2.1
-      function.prototype.name: 1.1.5
+      function.prototype.name: 1.1.6
       get-intrinsic: 1.2.1
       get-symbol-description: 1.0.0
       globalthis: 1.0.3
@@ -2149,19 +2269,42 @@ packages:
       is-regex: 1.1.4
       is-shared-array-buffer: 1.0.2
       is-string: 1.0.7
-      is-typed-array: 1.1.10
+      is-typed-array: 1.1.12
       is-weakref: 1.0.2
       object-inspect: 1.12.3
       object-keys: 1.1.1
       object.assign: 4.1.4
-      regexp.prototype.flags: 1.5.0
+      regexp.prototype.flags: 1.5.1
+      safe-array-concat: 1.0.1
       safe-regex-test: 1.0.0
-      string.prototype.trim: 1.2.7
-      string.prototype.trimend: 1.0.6
-      string.prototype.trimstart: 1.0.6
+      string.prototype.trim: 1.2.8
+      string.prototype.trimend: 1.0.7
+      string.prototype.trimstart: 1.0.7
+      typed-array-buffer: 1.0.0
+      typed-array-byte-length: 1.0.0
+      typed-array-byte-offset: 1.0.0
       typed-array-length: 1.0.4
       unbox-primitive: 1.0.2
-      which-typed-array: 1.1.9
+      which-typed-array: 1.1.11
+    dev: true
+
+  /es-iterator-helpers@1.0.15:
+    resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==}
+    dependencies:
+      asynciterator.prototype: 1.0.0
+      call-bind: 1.0.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
+      es-set-tostringtag: 2.0.1
+      function-bind: 1.1.1
+      get-intrinsic: 1.2.1
+      globalthis: 1.0.3
+      has-property-descriptors: 1.0.0
+      has-proto: 1.0.1
+      has-symbols: 1.0.3
+      internal-slot: 1.0.5
+      iterator.prototype: 1.1.2
+      safe-array-concat: 1.0.1
     dev: true
 
   /es-set-tostringtag@2.0.1:
@@ -2203,96 +2346,93 @@ packages:
     engines: {node: '>=10'}
     dev: true
 
-  /eslint-config-prettier@8.8.0(eslint@8.44.0):
-    resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
+  /eslint-config-prettier@9.0.0(eslint@8.49.0):
+    resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 8.44.0
+      eslint: 8.49.0
     dev: true
 
-  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0)(eslint@8.44.0)(prettier@3.0.0):
-    resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
-    engines: {node: '>=12.0.0'}
+  /eslint-plugin-prettier@5.0.0(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3):
+    resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==}
+    engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
-      eslint: '>=7.28.0'
+      '@types/eslint': '>=8.0.0'
+      eslint: '>=8.0.0'
       eslint-config-prettier: '*'
-      prettier: '>=2.0.0'
+      prettier: '>=3.0.0'
     peerDependenciesMeta:
+      '@types/eslint':
+        optional: true
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 8.44.0
-      eslint-config-prettier: 8.8.0(eslint@8.44.0)
-      prettier: 3.0.0
+      eslint: 8.49.0
+      eslint-config-prettier: 9.0.0(eslint@8.49.0)
+      prettier: 3.0.3
       prettier-linter-helpers: 1.0.0
+      synckit: 0.8.5
     dev: true
 
-  /eslint-plugin-react-hooks@4.6.0(eslint@8.44.0):
+  /eslint-plugin-react-hooks@4.6.0(eslint@8.49.0):
     resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
     engines: {node: '>=10'}
     peerDependencies:
       eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
     dependencies:
-      eslint: 8.44.0
+      eslint: 8.49.0
     dev: true
 
-  /eslint-plugin-react@7.32.2(eslint@8.44.0):
-    resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
+  /eslint-plugin-react@7.33.2(eslint@8.49.0):
+    resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==}
     engines: {node: '>=4'}
     peerDependencies:
       eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
     dependencies:
-      array-includes: 3.1.6
-      array.prototype.flatmap: 1.3.1
-      array.prototype.tosorted: 1.1.1
+      array-includes: 3.1.7
+      array.prototype.flatmap: 1.3.2
+      array.prototype.tosorted: 1.1.2
       doctrine: 2.1.0
-      eslint: 8.44.0
+      es-iterator-helpers: 1.0.15
+      eslint: 8.49.0
       estraverse: 5.3.0
-      jsx-ast-utils: 3.3.4
+      jsx-ast-utils: 3.3.5
       minimatch: 3.1.2
-      object.entries: 1.1.6
-      object.fromentries: 2.0.6
-      object.hasown: 1.1.2
-      object.values: 1.1.6
+      object.entries: 1.1.7
+      object.fromentries: 2.0.7
+      object.hasown: 1.1.3
+      object.values: 1.1.7
       prop-types: 15.8.1
       resolve: 2.0.0-next.4
-      semver: 6.3.0
-      string.prototype.matchall: 4.0.8
+      semver: 6.3.1
+      string.prototype.matchall: 4.0.10
     dev: true
 
-  /eslint-scope@5.1.1:
-    resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
-    engines: {node: '>=8.0.0'}
-    dependencies:
-      esrecurse: 4.3.0
-      estraverse: 4.3.0
-    dev: true
-
-  /eslint-scope@7.2.0:
-    resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==}
+  /eslint-scope@7.2.2:
+    resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
       esrecurse: 4.3.0
       estraverse: 5.3.0
     dev: true
 
-  /eslint-visitor-keys@3.4.1:
-    resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==}
+  /eslint-visitor-keys@3.4.3:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
-  /eslint@8.44.0:
-    resolution: {integrity: sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==}
+  /eslint@8.49.0:
+    resolution: {integrity: sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     hasBin: true
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.44.0)
-      '@eslint-community/regexpp': 4.5.1
-      '@eslint/eslintrc': 2.1.0
-      '@eslint/js': 8.44.0
-      '@humanwhocodes/config-array': 0.11.10
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.49.0)
+      '@eslint-community/regexpp': 4.8.1
+      '@eslint/eslintrc': 2.1.2
+      '@eslint/js': 8.49.0
+      '@humanwhocodes/config-array': 0.11.11
       '@humanwhocodes/module-importer': 1.0.1
       '@nodelib/fs.walk': 1.2.8
       ajv: 6.12.6
@@ -2301,19 +2441,18 @@ packages:
       debug: 4.3.4
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
-      eslint-scope: 7.2.0
-      eslint-visitor-keys: 3.4.1
-      espree: 9.6.0
+      eslint-scope: 7.2.2
+      eslint-visitor-keys: 3.4.3
+      espree: 9.6.1
       esquery: 1.5.0
       esutils: 2.0.3
       fast-deep-equal: 3.1.3
       file-entry-cache: 6.0.1
       find-up: 5.0.0
       glob-parent: 6.0.2
-      globals: 13.20.0
+      globals: 13.21.0
       graphemer: 1.4.0
       ignore: 5.2.4
-      import-fresh: 3.3.0
       imurmurhash: 0.1.4
       is-glob: 4.0.3
       is-path-inside: 3.0.3
@@ -2325,19 +2464,18 @@ packages:
       natural-compare: 1.4.0
       optionator: 0.9.3
       strip-ansi: 6.0.1
-      strip-json-comments: 3.1.1
       text-table: 0.2.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /espree@9.6.0:
-    resolution: {integrity: sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==}
+  /espree@9.6.1:
+    resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
       acorn: 8.10.0
       acorn-jsx: 5.3.2(acorn@8.10.0)
-      eslint-visitor-keys: 3.4.1
+      eslint-visitor-keys: 3.4.3
     dev: true
 
   /esquery@1.5.0:
@@ -2354,11 +2492,6 @@ packages:
       estraverse: 5.3.0
     dev: true
 
-  /estraverse@4.3.0:
-    resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
-    engines: {node: '>=4.0'}
-    dev: true
-
   /estraverse@5.3.0:
     resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
     engines: {node: '>=4.0'}
@@ -2379,6 +2512,36 @@ packages:
     engines: {node: '>=12.0.0'}
     dev: false
 
+  /execa@5.1.1:
+    resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
+    engines: {node: '>=10'}
+    dependencies:
+      cross-spawn: 7.0.3
+      get-stream: 6.0.1
+      human-signals: 2.1.0
+      is-stream: 2.0.1
+      merge-stream: 2.0.0
+      npm-run-path: 4.0.1
+      onetime: 5.1.2
+      signal-exit: 3.0.7
+      strip-final-newline: 2.0.0
+    dev: true
+
+  /execa@7.2.0:
+    resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
+    engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
+    dependencies:
+      cross-spawn: 7.0.3
+      get-stream: 6.0.1
+      human-signals: 4.3.1
+      is-stream: 3.0.0
+      merge-stream: 2.0.0
+      npm-run-path: 5.1.0
+      onetime: 6.0.0
+      signal-exit: 3.0.7
+      strip-final-newline: 3.0.0
+    dev: true
+
   /fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     dev: true
@@ -2387,8 +2550,8 @@ packages:
     resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
     dev: true
 
-  /fast-glob@3.3.0:
-    resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==}
+  /fast-glob@3.3.1:
+    resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
     engines: {node: '>=8.6.0'}
     dependencies:
       '@nodelib/fs.stat': 2.0.5
@@ -2423,7 +2586,7 @@ packages:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
     dependencies:
-      flat-cache: 3.0.4
+      flat-cache: 3.1.0
     dev: true
 
   /fill-range@7.0.1:
@@ -2441,20 +2604,21 @@ packages:
       path-exists: 4.0.0
     dev: true
 
-  /flat-cache@3.0.4:
-    resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
-    engines: {node: ^10.12.0 || >=12.0.0}
+  /flat-cache@3.1.0:
+    resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==}
+    engines: {node: '>=12.0.0'}
     dependencies:
-      flatted: 3.2.7
+      flatted: 3.2.9
+      keyv: 4.5.3
       rimraf: 3.0.2
     dev: true
 
-  /flatted@3.2.7:
-    resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
+  /flatted@3.2.9:
+    resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
     dev: true
 
-  /follow-redirects@1.15.2:
-    resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
+  /follow-redirects@1.15.3:
+    resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
     engines: {node: '>=4.0'}
     peerDependencies:
       debug: '*'
@@ -2486,13 +2650,13 @@ packages:
     resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
     dev: true
 
-  /function.prototype.name@1.1.5:
-    resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
+  /function.prototype.name@1.1.6:
+    resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
       functions-have-names: 1.2.3
     dev: true
 
@@ -2514,6 +2678,11 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
+  /get-stream@6.0.1:
+    resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+    engines: {node: '>=10'}
+    dev: true
+
   /get-symbol-description@1.0.0:
     resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
     engines: {node: '>= 0.4'}
@@ -2547,8 +2716,8 @@ packages:
       path-is-absolute: 1.0.1
     dev: true
 
-  /globals@13.20.0:
-    resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==}
+  /globals@13.21.0:
+    resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==}
     engines: {node: '>=8'}
     dependencies:
       type-fest: 0.20.2
@@ -2558,7 +2727,7 @@ packages:
     resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
     engines: {node: '>= 0.4'}
     dependencies:
-      define-properties: 1.2.0
+      define-properties: 1.2.1
     dev: true
 
   /globby@11.1.0:
@@ -2567,7 +2736,7 @@ packages:
     dependencies:
       array-union: 2.1.0
       dir-glob: 3.0.1
-      fast-glob: 3.3.0
+      fast-glob: 3.3.1
       ignore: 5.2.4
       merge2: 1.4.1
       slash: 3.0.0
@@ -2633,7 +2802,7 @@ packages:
       void-elements: 3.1.0
     dev: false
 
-  /htmlnano@2.0.4(svgo@2.8.0):
+  /htmlnano@2.0.4(svgo@2.8.0)(typescript@5.2.2):
     resolution: {integrity: sha512-WGCkyGFwjKW1GeCBsPYacMvaMnZtFJ0zIRnC2NCddkA+IOEhTqskXrS7lep+3yYZw/nQ3dW1UAX4yA/GJyR8BA==}
     peerDependencies:
       cssnano: ^6.0.0
@@ -2662,10 +2831,12 @@ packages:
       uncss:
         optional: true
     dependencies:
-      cosmiconfig: 8.2.0
+      cosmiconfig: 8.3.6(typescript@5.2.2)
       posthtml: 0.16.6
       svgo: 2.8.0
       timsort: 0.3.0
+    transitivePeerDependencies:
+      - typescript
     dev: true
 
   /htmlparser2@7.2.0:
@@ -2677,6 +2848,16 @@ packages:
       entities: 3.0.1
     dev: true
 
+  /human-signals@2.1.0:
+    resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
+    engines: {node: '>=10.17.0'}
+    dev: true
+
+  /human-signals@4.3.1:
+    resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==}
+    engines: {node: '>=14.18.0'}
+    dev: true
+
   /hyphenate-style-name@1.0.4:
     resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
     dev: false
@@ -2684,13 +2865,13 @@ packages:
   /i18next-browser-languagedetector@7.1.0:
     resolution: {integrity: sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==}
     dependencies:
-      '@babel/runtime': 7.22.6
+      '@babel/runtime': 7.22.15
     dev: false
 
-  /i18next@23.2.8:
-    resolution: {integrity: sha512-wU0pMlJ91ZbB89i77G3YQ11/pBQrzgWpxJYl7HFyA9aU9v3aHMI/oBKQmAJNURr0A8cLG4EHjgSMK8IqQTp4PQ==}
+  /i18next@23.5.1:
+    resolution: {integrity: sha512-JelYzcaCoFDaa+Ysbfz2JsGAKkrHiMG6S61+HLBUEIPaF40WMwW9hCPymlQGrP+wWawKxKPuSuD71WZscCsWHg==}
     dependencies:
-      '@babel/runtime': 7.22.6
+      '@babel/runtime': 7.22.15
     dev: false
 
   /ieee754@1.2.1:
@@ -2740,7 +2921,7 @@ packages:
     dependencies:
       call-bind: 1.0.2
       get-intrinsic: 1.2.1
-      is-typed-array: 1.1.10
+      is-typed-array: 1.1.12
     dev: true
 
   /is-arrayish@0.2.1:
@@ -2751,6 +2932,13 @@ packages:
     resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
     dev: false
 
+  /is-async-function@2.0.0:
+    resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      has-tostringtag: 1.0.0
+    dev: true
+
   /is-bigint@1.0.4:
     resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
     dependencies:
@@ -2770,8 +2958,8 @@ packages:
     engines: {node: '>= 0.4'}
     dev: true
 
-  /is-core-module@2.12.1:
-    resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
+  /is-core-module@2.13.0:
+    resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==}
     dependencies:
       has: 1.0.3
     dev: true
@@ -2783,11 +2971,36 @@ packages:
       has-tostringtag: 1.0.0
     dev: true
 
+  /is-docker@2.2.1:
+    resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
+    engines: {node: '>=8'}
+    hasBin: true
+    dev: true
+
+  /is-docker@3.0.0:
+    resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    hasBin: true
+    dev: true
+
   /is-extglob@2.1.1:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /is-finalizationregistry@1.0.2:
+    resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==}
+    dependencies:
+      call-bind: 1.0.2
+    dev: true
+
+  /is-generator-function@1.0.10:
+    resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      has-tostringtag: 1.0.0
+    dev: true
+
   /is-glob@4.0.3:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
@@ -2795,10 +3008,22 @@ packages:
       is-extglob: 2.1.1
     dev: true
 
+  /is-inside-container@1.0.0:
+    resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+    engines: {node: '>=14.16'}
+    hasBin: true
+    dependencies:
+      is-docker: 3.0.0
+    dev: true
+
   /is-json@2.0.1:
     resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==}
     dev: true
 
+  /is-map@2.0.2:
+    resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
+    dev: true
+
   /is-negative-zero@2.0.2:
     resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
     engines: {node: '>= 0.4'}
@@ -2829,12 +3054,26 @@ packages:
       has-tostringtag: 1.0.0
     dev: true
 
+  /is-set@2.0.2:
+    resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==}
+    dev: true
+
   /is-shared-array-buffer@1.0.2:
     resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
     dependencies:
       call-bind: 1.0.2
     dev: true
 
+  /is-stream@2.0.1:
+    resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+    engines: {node: '>=8'}
+    dev: true
+
+  /is-stream@3.0.0:
+    resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dev: true
+
   /is-string@1.0.7:
     resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
     engines: {node: '>= 0.4'}
@@ -2849,15 +3088,15 @@ packages:
       has-symbols: 1.0.3
     dev: true
 
-  /is-typed-array@1.1.10:
-    resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==}
+  /is-typed-array@1.1.12:
+    resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==}
     engines: {node: '>= 0.4'}
     dependencies:
-      available-typed-arrays: 1.0.5
-      call-bind: 1.0.2
-      for-each: 0.3.3
-      gopd: 1.0.1
-      has-tostringtag: 1.0.0
+      which-typed-array: 1.1.11
+    dev: true
+
+  /is-weakmap@2.0.1:
+    resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}
     dev: true
 
   /is-weakref@1.0.2:
@@ -2866,10 +3105,38 @@ packages:
       call-bind: 1.0.2
     dev: true
 
+  /is-weakset@2.0.2:
+    resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==}
+    dependencies:
+      call-bind: 1.0.2
+      get-intrinsic: 1.2.1
+    dev: true
+
+  /is-wsl@2.2.0:
+    resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
+    engines: {node: '>=8'}
+    dependencies:
+      is-docker: 2.2.1
+    dev: true
+
+  /isarray@2.0.5:
+    resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+    dev: true
+
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
     dev: true
 
+  /iterator.prototype@1.1.2:
+    resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
+    dependencies:
+      define-properties: 1.2.1
+      get-intrinsic: 1.2.1
+      has-symbols: 1.0.3
+      reflect.getprototypeof: 1.0.4
+      set-function-name: 2.0.1
+    dev: true
+
   /js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -2880,6 +3147,10 @@ packages:
       argparse: 2.0.1
     dev: true
 
+  /json-buffer@3.0.1:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+    dev: true
+
   /json-parse-even-better-errors@2.3.1:
     resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
     dev: true
@@ -2898,14 +3169,20 @@ packages:
     hasBin: true
     dev: true
 
-  /jsx-ast-utils@3.3.4:
-    resolution: {integrity: sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==}
+  /jsx-ast-utils@3.3.5:
+    resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
     engines: {node: '>=4.0'}
     dependencies:
-      array-includes: 3.1.6
-      array.prototype.flat: 1.3.1
+      array-includes: 3.1.7
+      array.prototype.flat: 1.3.2
       object.assign: 4.1.4
-      object.values: 1.1.6
+      object.values: 1.1.7
+    dev: true
+
+  /keyv@4.5.3:
+    resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==}
+    dependencies:
+      json-buffer: 3.0.1
     dev: true
 
   /levn@0.4.1:
@@ -2916,8 +3193,8 @@ packages:
       type-check: 0.4.0
     dev: true
 
-  /lightningcss-darwin-arm64@1.21.5:
-    resolution: {integrity: sha512-z05hyLX85WY0UfhkFUOrWEFqD69lpVAmgl3aDzMKlIZJGygbhbegqb4PV8qfUrKKNBauut/qVNPKZglhTaDDxA==}
+  /lightningcss-darwin-arm64@1.22.0:
+    resolution: {integrity: sha512-aH2be3nNny+It5YEVm8tBSSdRlBVWQV8m2oJ7dESiYRzyY/E/bQUe2xlw5caaMuhlM9aoTMtOH25yzMhir0qPg==}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     os: [darwin]
@@ -2925,8 +3202,8 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-darwin-x64@1.21.5:
-    resolution: {integrity: sha512-MSJhmej/U9MrdPxDk7+FWhO8+UqVoZUHG4VvKT5RQ4RJtqtANTiWiI97LvoVNMtdMnHaKs1Pkji6wHUFxjJsHQ==}
+  /lightningcss-darwin-x64@1.22.0:
+    resolution: {integrity: sha512-9KHRFA0Y6mNxRHeoQMp0YaI0R0O2kOgUlYPRjuasU4d+pI8NRhVn9bt0yX9VPs5ibWX1RbDViSPtGJvYYrfVAQ==}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     os: [darwin]
@@ -2934,8 +3211,17 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-linux-arm-gnueabihf@1.21.5:
-    resolution: {integrity: sha512-xN6+5/JsMrbZHL1lPl+MiNJ3Xza12ueBKPepiyDCFQzlhFRTj7D0LG+cfNTzPBTO8KcYQynLpl1iBB8LGp3Xtw==}
+  /lightningcss-freebsd-x64@1.22.0:
+    resolution: {integrity: sha512-xaYL3xperGwD85rQioDb52ozF3NAJb+9wrge3jD9lxGffplu0Mn35rXMptB8Uc2N9Mw1i3Bvl7+z1evlqVl7ww==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /lightningcss-linux-arm-gnueabihf@1.22.0:
+    resolution: {integrity: sha512-epQGvXIjOuxrZpMpMnRjK54ZqzhiHhCPLtHvw2fb6NeK2kK9YtF0wqmeTBiQ1AkbWfnnXGTstYaFNiadNK+StQ==}
     engines: {node: '>= 12.0.0'}
     cpu: [arm]
     os: [linux]
@@ -2943,8 +3229,8 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-linux-arm64-gnu@1.21.5:
-    resolution: {integrity: sha512-KfzFNhC4XTbmG3ma/xcTs/IhCwieW89XALIusKmnV0N618ZDXEB0XjWOYQRCXeK9mfqPdbTBpurEHV/XZtkniQ==}
+  /lightningcss-linux-arm64-gnu@1.22.0:
+    resolution: {integrity: sha512-AArGtKSY4DGTA8xP8SDyNyKtpsUl1Rzq6FW4JomeyUQ4nBrR71uPChksTpj3gmWuGhZeRKLeCUI1DBid/zhChg==}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     os: [linux]
@@ -2952,8 +3238,8 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-linux-arm64-musl@1.21.5:
-    resolution: {integrity: sha512-bc0GytQO5Mn9QM6szaZ+31fQHNdidgpM1sSCwzPItz8hg3wOvKl8039rU0veMJV3ZgC9z0ypNRceLrSHeRHmXw==}
+  /lightningcss-linux-arm64-musl@1.22.0:
+    resolution: {integrity: sha512-RRraNgP8hnBPhInTTUdlFm+z16C/ghbxBG51Sw00hd7HUyKmEUKRozyc5od+/N6pOrX/bIh5vIbtMXIxsos0lg==}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     os: [linux]
@@ -2961,8 +3247,8 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-linux-x64-gnu@1.21.5:
-    resolution: {integrity: sha512-JwMbgypPQgc2kW2av3OwzZ8cbrEuIiDiXPJdXRE6aVxu67yHauJawQLqJKTGUhiAhy6iLDG8Wg0a3/ziL+m+Kw==}
+  /lightningcss-linux-x64-gnu@1.22.0:
+    resolution: {integrity: sha512-grdrhYGRi2KrR+bsXJVI0myRADqyA7ekprGxiuK5QRNkv7kj3Yq1fERDNyzZvjisHwKUi29sYMClscbtl+/Zpw==}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     os: [linux]
@@ -2970,8 +3256,8 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-linux-x64-musl@1.21.5:
-    resolution: {integrity: sha512-Ib8b6IQ/OR/VrPU6YBgy4T3QnuHY7DUa95O+nz+cwrTkMSN6fuHcTcIaz4t8TJ6HI5pl3uxUOZjmtls2pyQWow==}
+  /lightningcss-linux-x64-musl@1.22.0:
+    resolution: {integrity: sha512-t5f90X+iQUtIyR56oXIHMBUyQFX/zwmPt72E6Dane3P8KNGlkijTg2I75XVQS860gNoEFzV7Mm5ArRRA7u5CAQ==}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     os: [linux]
@@ -2979,8 +3265,8 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss-win32-x64-msvc@1.21.5:
-    resolution: {integrity: sha512-A8cSi8lUpBeVmoF+DqqW7cd0FemDbCuKr490IXdjyeI+KL8adpSKUs8tcqO0OXPh1EoDqK7JNkD/dELmd4Iz5g==}
+  /lightningcss-win32-x64-msvc@1.22.0:
+    resolution: {integrity: sha512-64HTDtOOZE9PUCZJiZZQpyqXBbdby1lnztBccnqh+NtbKxjnGzP92R2ngcgeuqMPecMNqNWxgoWgTGpC+yN5Sw==}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     os: [win32]
@@ -2988,20 +3274,21 @@ packages:
     dev: true
     optional: true
 
-  /lightningcss@1.21.5:
-    resolution: {integrity: sha512-/pEUPeih2EwIx9n4T82aOG6CInN83tl/mWlw6B5gWLf36UplQi1L+5p3FUHsdt4fXVfOkkh9KIaM3owoq7ss8A==}
+  /lightningcss@1.22.0:
+    resolution: {integrity: sha512-+z0qvwRVzs4XGRXelnWRNwqsXUx8k3bSkbP8vD42kYKSk3z9OM2P3e/gagT7ei/gwh8DTS80LZOFZV6lm8Z8Fg==}
     engines: {node: '>= 12.0.0'}
     dependencies:
       detect-libc: 1.0.3
     optionalDependencies:
-      lightningcss-darwin-arm64: 1.21.5
-      lightningcss-darwin-x64: 1.21.5
-      lightningcss-linux-arm-gnueabihf: 1.21.5
-      lightningcss-linux-arm64-gnu: 1.21.5
-      lightningcss-linux-arm64-musl: 1.21.5
-      lightningcss-linux-x64-gnu: 1.21.5
-      lightningcss-linux-x64-musl: 1.21.5
-      lightningcss-win32-x64-msvc: 1.21.5
+      lightningcss-darwin-arm64: 1.22.0
+      lightningcss-darwin-x64: 1.22.0
+      lightningcss-freebsd-x64: 1.22.0
+      lightningcss-linux-arm-gnueabihf: 1.22.0
+      lightningcss-linux-arm64-gnu: 1.22.0
+      lightningcss-linux-arm64-musl: 1.22.0
+      lightningcss-linux-x64-gnu: 1.22.0
+      lightningcss-linux-x64-musl: 1.22.0
+      lightningcss-win32-x64-msvc: 1.22.0
     dev: true
 
   /lines-and-columns@1.2.4:
@@ -3059,9 +3346,9 @@ packages:
     resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
     dev: true
 
-  /marked@5.1.1:
-    resolution: {integrity: sha512-bTmmGdEINWmOMDjnPWDxGPQ4qkDLeYorpYbEtFOXzOruTwUE671q4Guiuchn4N8h/v6NGd7916kXsm3Iz4iUSg==}
-    engines: {node: '>= 18'}
+  /marked@9.0.3:
+    resolution: {integrity: sha512-pI/k4nzBG1PEq1J3XFEHxVvjicfjl8rgaMaqclouGSMPhk7Q3Ejb2ZRxx/ZQOcQ1909HzVoWCFYq6oLgtL4BpQ==}
+    engines: {node: '>= 16'}
     hasBin: true
     dev: false
 
@@ -3075,6 +3362,10 @@ packages:
     resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
     dev: true
 
+  /merge-stream@2.0.0:
+    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+    dev: true
+
   /merge2@1.4.1:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
@@ -3100,6 +3391,16 @@ packages:
       mime-db: 1.52.0
     dev: false
 
+  /mimic-fn@2.1.0:
+    resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+    engines: {node: '>=6'}
+    dev: true
+
+  /mimic-fn@4.0.0:
+    resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
+    engines: {node: '>=12'}
+    dev: true
+
   /minimatch@3.1.2:
     resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
     dependencies:
@@ -3136,16 +3437,12 @@ packages:
       msgpackr-extract: 3.0.2
     dev: true
 
-  /msgpackr@1.9.5:
-    resolution: {integrity: sha512-/IJ3cFSN6Ci3eG2wLhbFEL6GT63yEaoN/R5My2QkV6zro+OJaVRLPlwvxY7EtHYSmDlQpk8stvOQTL2qJFkDRg==}
+  /msgpackr@1.9.9:
+    resolution: {integrity: sha512-sbn6mioS2w0lq1O6PpGtsv6Gy8roWM+o3o4Sqjd6DudrL/nOugY+KyJUimoWzHnf9OkO0T6broHFnYE/R05t9A==}
     optionalDependencies:
       msgpackr-extract: 3.0.2
     dev: true
 
-  /natural-compare-lite@1.4.0:
-    resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
-    dev: true
-
   /natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
     dev: true
@@ -3158,8 +3455,8 @@ packages:
     resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
     dev: true
 
-  /node-fetch@2.6.12:
-    resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
+  /node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
     engines: {node: 4.x || >=6.0.0}
     peerDependencies:
       encoding: ^0.1.0
@@ -3186,6 +3483,20 @@ packages:
     resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
     dev: true
 
+  /npm-run-path@4.0.1:
+    resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
+    engines: {node: '>=8'}
+    dependencies:
+      path-key: 3.1.1
+    dev: true
+
+  /npm-run-path@5.1.0:
+    resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dependencies:
+      path-key: 4.0.0
+    dev: true
+
   /nth-check@2.1.1:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
     dependencies:
@@ -3214,43 +3525,43 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
+      define-properties: 1.2.1
       has-symbols: 1.0.3
       object-keys: 1.1.1
     dev: true
 
-  /object.entries@1.1.6:
-    resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==}
+  /object.entries@1.1.7:
+    resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
-  /object.fromentries@2.0.6:
-    resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==}
+  /object.fromentries@2.0.7:
+    resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
-  /object.hasown@1.1.2:
-    resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==}
+  /object.hasown@1.1.3:
+    resolution: {integrity: sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==}
     dependencies:
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
-  /object.values@1.1.6:
-    resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==}
+  /object.values@1.1.7:
+    resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
   /once@1.4.0:
@@ -3259,6 +3570,30 @@ packages:
       wrappy: 1.0.2
     dev: true
 
+  /onetime@5.1.2:
+    resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+    engines: {node: '>=6'}
+    dependencies:
+      mimic-fn: 2.1.0
+    dev: true
+
+  /onetime@6.0.0:
+    resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
+    engines: {node: '>=12'}
+    dependencies:
+      mimic-fn: 4.0.0
+    dev: true
+
+  /open@9.1.0:
+    resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==}
+    engines: {node: '>=14.16'}
+    dependencies:
+      default-browser: 4.0.0
+      define-lazy-prop: 3.0.0
+      is-inside-container: 1.0.0
+      is-wsl: 2.2.0
+    dev: true
+
   /optionator@0.9.3:
     resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
     engines: {node: '>= 0.8.0'}
@@ -3289,7 +3624,7 @@ packages:
       p-limit: 3.1.0
     dev: true
 
-  /parcel@2.9.3:
+  /parcel@2.9.3(typescript@5.2.2):
     resolution: {integrity: sha512-2GTVocFkwblV/TIg9AmT7TI2fO4xdWkyN8aFUEVtiVNWt96GTR3FgQyHFValfCbcj1k9Xf962Ws2hYXYUr9k1Q==}
     engines: {node: '>= 12.0.0'}
     hasBin: true
@@ -3297,7 +3632,7 @@ packages:
       '@parcel/core':
         optional: true
     dependencies:
-      '@parcel/config-default': 2.9.3(@parcel/core@2.9.3)
+      '@parcel/config-default': 2.9.3(@parcel/core@2.9.3)(typescript@5.2.2)
       '@parcel/core': 2.9.3
       '@parcel/diagnostic': 2.9.3
       '@parcel/events': 2.9.3
@@ -3319,6 +3654,7 @@ packages:
       - relateurl
       - srcset
       - terser
+      - typescript
       - uncss
     dev: true
 
@@ -3333,7 +3669,7 @@ packages:
     resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
     engines: {node: '>=8'}
     dependencies:
-      '@babel/code-frame': 7.22.5
+      '@babel/code-frame': 7.22.13
       error-ex: 1.3.2
       json-parse-even-better-errors: 2.3.1
       lines-and-columns: 1.2.4
@@ -3354,6 +3690,11 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /path-key@4.0.0:
+    resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+    engines: {node: '>=12'}
+    dev: true
+
   /path-parse@1.0.7:
     resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
     dev: true
@@ -3417,8 +3758,8 @@ packages:
       fast-diff: 1.3.0
     dev: true
 
-  /prettier@3.0.0:
-    resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
+  /prettier@3.0.3:
+    resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==}
     engines: {node: '>=14'}
     hasBin: true
     dev: true
@@ -3473,8 +3814,8 @@ packages:
     resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
     dev: false
 
-  /react-i18next@13.0.1(i18next@23.2.8)(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-gMO6N2GfSfuH7xlHSsZ/mZf+Py9bLm/+EDKIn5fNTuDTjcCcwmMU5UEuGCDk5mdfivbo7ySyYXBN7B9tbGUxiA==}
+  /react-i18next@13.2.2(i18next@23.5.1)(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-+nFUkbRByFwnrfDcYqvzBuaeZb+nACHx+fAWN/pZMddWOCJH5hoc21+Sa/N/Lqi6ne6/9wC/qRGOoQhJa6IkEQ==}
     peerDependencies:
       i18next: '>= 23.2.3'
       react: '>= 16.8.0'
@@ -3486,9 +3827,9 @@ packages:
       react-native:
         optional: true
     dependencies:
-      '@babel/runtime': 7.22.6
+      '@babel/runtime': 7.22.15
       html-parse-stringify: 3.0.1
-      i18next: 23.2.8
+      i18next: 23.5.1
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: false
@@ -3528,26 +3869,26 @@ packages:
       shallow-equal: 1.2.1
     dev: false
 
-  /react-router-dom@6.14.1(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw==}
-    engines: {node: '>=14'}
+  /react-router-dom@6.16.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==}
+    engines: {node: '>=14.0.0'}
     peerDependencies:
       react: '>=16.8'
       react-dom: '>=16.8'
     dependencies:
-      '@remix-run/router': 1.7.1
+      '@remix-run/router': 1.9.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      react-router: 6.14.1(react@18.2.0)
+      react-router: 6.16.0(react@18.2.0)
     dev: false
 
-  /react-router@6.14.1(react@18.2.0):
-    resolution: {integrity: sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==}
-    engines: {node: '>=14'}
+  /react-router@6.16.0(react@18.2.0):
+    resolution: {integrity: sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==}
+    engines: {node: '>=14.0.0'}
     peerDependencies:
       react: '>=16.8'
     dependencies:
-      '@remix-run/router': 1.7.1
+      '@remix-run/router': 1.9.0
       react: 18.2.0
     dev: false
 
@@ -3557,7 +3898,7 @@ packages:
       react: '>=16.6.0'
       react-dom: '>=16.6.0'
     dependencies:
-      '@babel/runtime': 7.22.6
+      '@babel/runtime': 7.22.15
       dom-helpers: 5.2.1
       loose-envify: 1.4.0
       prop-types: 15.8.1
@@ -3572,16 +3913,33 @@ packages:
       loose-envify: 1.4.0
     dev: false
 
+  /reflect.getprototypeof@1.0.4:
+    resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      call-bind: 1.0.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
+      get-intrinsic: 1.2.1
+      globalthis: 1.0.3
+      which-builtin-type: 1.1.3
+    dev: true
+
   /regenerator-runtime@0.13.11:
     resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+    dev: true
+
+  /regenerator-runtime@0.14.0:
+    resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
+    dev: false
 
-  /regexp.prototype.flags@1.5.0:
-    resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
+  /regexp.prototype.flags@1.5.1:
+    resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      functions-have-names: 1.2.3
+      define-properties: 1.2.1
+      set-function-name: 2.0.1
     dev: true
 
   /requires-port@1.0.0:
@@ -3597,7 +3955,7 @@ packages:
     resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==}
     hasBin: true
     dependencies:
-      is-core-module: 2.12.1
+      is-core-module: 2.13.0
       path-parse: 1.0.7
       supports-preserve-symlinks-flag: 1.0.0
     dev: true
@@ -3614,6 +3972,13 @@ packages:
       glob: 7.2.3
     dev: true
 
+  /run-applescript@5.0.0:
+    resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==}
+    engines: {node: '>=12'}
+    dependencies:
+      execa: 5.1.1
+    dev: true
+
   /run-parallel@1.2.0:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
     dependencies:
@@ -3623,9 +3988,19 @@ packages:
   /rxjs@7.8.1:
     resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
     dependencies:
-      tslib: 2.6.0
+      tslib: 2.6.2
     dev: false
 
+  /safe-array-concat@1.0.1:
+    resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==}
+    engines: {node: '>=0.4'}
+    dependencies:
+      call-bind: 1.0.2
+      get-intrinsic: 1.2.1
+      has-symbols: 1.0.3
+      isarray: 2.0.5
+    dev: true
+
   /safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
     dev: true
@@ -3644,8 +4019,8 @@ packages:
       loose-envify: 1.4.0
     dev: false
 
-  /semver@6.3.0:
-    resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+  /semver@6.3.1:
+    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
     hasBin: true
     dev: true
 
@@ -3661,6 +4036,15 @@ packages:
     resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
     dev: false
 
+  /set-function-name@2.0.1:
+    resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      define-data-property: 1.1.0
+      functions-have-names: 1.2.3
+      has-property-descriptors: 1.0.0
+    dev: true
+
   /shallow-equal@1.2.1:
     resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
     dev: false
@@ -3685,6 +4069,10 @@ packages:
       object-inspect: 1.12.3
     dev: true
 
+  /signal-exit@3.0.7:
+    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+    dev: true
+
   /simple-swizzle@0.2.2:
     resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
     dependencies:
@@ -3711,42 +4099,43 @@ packages:
     deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
     dev: true
 
-  /string.prototype.matchall@4.0.8:
-    resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==}
+  /string.prototype.matchall@4.0.10:
+    resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
       get-intrinsic: 1.2.1
       has-symbols: 1.0.3
       internal-slot: 1.0.5
-      regexp.prototype.flags: 1.5.0
+      regexp.prototype.flags: 1.5.1
+      set-function-name: 2.0.1
       side-channel: 1.0.4
     dev: true
 
-  /string.prototype.trim@1.2.7:
-    resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==}
+  /string.prototype.trim@1.2.8:
+    resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==}
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
-  /string.prototype.trimend@1.0.6:
-    resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
+  /string.prototype.trimend@1.0.7:
+    resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
-  /string.prototype.trimstart@1.0.6:
-    resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==}
+  /string.prototype.trimstart@1.0.7:
+    resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==}
     dependencies:
       call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.21.2
+      define-properties: 1.2.1
+      es-abstract: 1.22.2
     dev: true
 
   /strip-ansi@6.0.1:
@@ -3756,6 +4145,16 @@ packages:
       ansi-regex: 5.0.1
     dev: true
 
+  /strip-final-newline@2.0.0:
+    resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
+    engines: {node: '>=6'}
+    dev: true
+
+  /strip-final-newline@3.0.0:
+    resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
+    engines: {node: '>=12'}
+    dev: true
+
   /strip-json-comments@3.1.1:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
@@ -3794,6 +4193,14 @@ packages:
       stable: 0.1.8
     dev: true
 
+  /synckit@0.8.5:
+    resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==}
+    engines: {node: ^14.18.0 || >=16.0.0}
+    dependencies:
+      '@pkgr/utils': 2.4.2
+      tslib: 2.6.2
+    dev: true
+
   /term-size@2.2.1:
     resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
     engines: {node: '>=8'}
@@ -3807,6 +4214,11 @@ packages:
     resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==}
     dev: true
 
+  /titleize@3.0.0:
+    resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==}
+    engines: {node: '>=12'}
+    dev: true
+
   /to-regex-range@5.0.1:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
@@ -3828,7 +4240,16 @@ packages:
     resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
     dev: false
 
-  /ts-node@10.9.1(@types/node@20.4.1)(typescript@5.1.6):
+  /ts-api-utils@1.0.3(typescript@5.2.2):
+    resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
+    engines: {node: '>=16.13.0'}
+    peerDependencies:
+      typescript: '>=4.2.0'
+    dependencies:
+      typescript: 5.2.2
+    dev: true
+
+  /ts-node@10.9.1(@types/node@20.6.3)(typescript@5.2.2):
     resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
     hasBin: true
     peerDependencies:
@@ -3847,34 +4268,20 @@ packages:
       '@tsconfig/node12': 1.0.11
       '@tsconfig/node14': 1.0.3
       '@tsconfig/node16': 1.0.4
-      '@types/node': 20.4.1
+      '@types/node': 20.6.3
       acorn: 8.10.0
       acorn-walk: 8.2.0
       arg: 4.1.3
       create-require: 1.1.1
       diff: 4.0.2
       make-error: 1.3.6
-      typescript: 5.1.6
+      typescript: 5.2.2
       v8-compile-cache-lib: 3.0.1
       yn: 3.1.1
     dev: true
 
-  /tslib@1.14.1:
-    resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
-    dev: true
-
-  /tslib@2.6.0:
-    resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==}
-
-  /tsutils@3.21.0(typescript@5.1.6):
-    resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
-    engines: {node: '>= 6'}
-    peerDependencies:
-      typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
-    dependencies:
-      tslib: 1.14.1
-      typescript: 5.1.6
-    dev: true
+  /tslib@2.6.2:
+    resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
 
   /type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
@@ -3888,12 +4295,42 @@ packages:
     engines: {node: '>=10'}
     dev: true
 
+  /typed-array-buffer@1.0.0:
+    resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      call-bind: 1.0.2
+      get-intrinsic: 1.2.1
+      is-typed-array: 1.1.12
+    dev: true
+
+  /typed-array-byte-length@1.0.0:
+    resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      call-bind: 1.0.2
+      for-each: 0.3.3
+      has-proto: 1.0.1
+      is-typed-array: 1.1.12
+    dev: true
+
+  /typed-array-byte-offset@1.0.0:
+    resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      available-typed-arrays: 1.0.5
+      call-bind: 1.0.2
+      for-each: 0.3.3
+      has-proto: 1.0.1
+      is-typed-array: 1.1.12
+    dev: true
+
   /typed-array-length@1.0.4:
     resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
     dependencies:
       call-bind: 1.0.2
       for-each: 0.3.3
-      is-typed-array: 1.1.10
+      is-typed-array: 1.1.12
     dev: true
 
   /typescript-language-server@3.3.2:
@@ -3902,11 +4339,11 @@ packages:
     hasBin: true
     dependencies:
       vscode-jsonrpc: 5.0.1
-      vscode-languageserver-protocol: 3.17.3
+      vscode-languageserver-protocol: 3.17.4
     dev: true
 
-  /typescript@5.1.6:
-    resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
+  /typescript@5.2.2:
+    resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
     engines: {node: '>=14.17'}
     hasBin: true
     dev: true
@@ -3925,13 +4362,18 @@ packages:
     engines: {node: '>= 4.0.0'}
     dev: false
 
-  /update-browserslist-db@1.0.11(browserslist@4.21.9):
+  /untildify@4.0.0:
+    resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
+    engines: {node: '>=8'}
+    dev: true
+
+  /update-browserslist-db@1.0.11(browserslist@4.21.10):
     resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
     hasBin: true
     peerDependencies:
       browserslist: '>= 4.21.0'
     dependencies:
-      browserslist: 4.21.9
+      browserslist: 4.21.10
       escalade: 3.1.1
       picocolors: 1.0.0
     dev: true
@@ -3968,20 +4410,20 @@ packages:
     engines: {node: '>=8.0.0 || >=10.0.0'}
     dev: true
 
-  /vscode-jsonrpc@8.1.0:
-    resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==}
+  /vscode-jsonrpc@8.2.0:
+    resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
     engines: {node: '>=14.0.0'}
     dev: true
 
-  /vscode-languageserver-protocol@3.17.3:
-    resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==}
+  /vscode-languageserver-protocol@3.17.4:
+    resolution: {integrity: sha512-IpaHLPft+UBWf4dOIH15YEgydTbXGz52EMU2h16SfFpYu/yOQt3pY14049mtpJu+4CBHn+hq7S67e7O0AwpRqQ==}
     dependencies:
-      vscode-jsonrpc: 8.1.0
-      vscode-languageserver-types: 3.17.3
+      vscode-jsonrpc: 8.2.0
+      vscode-languageserver-types: 3.17.4
     dev: true
 
-  /vscode-languageserver-types@3.17.3:
-    resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==}
+  /vscode-languageserver-types@3.17.4:
+    resolution: {integrity: sha512-9YXi5pA3XF2V+NUQg6g+lulNS0ncRCKASYdK3Cs7kiH9sVFXWq27prjkC/B8M/xJLRPPRSPCHVMuBTgRNFh2sQ==}
     dev: true
 
   /warning@4.0.3:
@@ -4015,8 +4457,35 @@ packages:
       is-symbol: 1.0.4
     dev: true
 
-  /which-typed-array@1.1.9:
-    resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==}
+  /which-builtin-type@1.1.3:
+    resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      function.prototype.name: 1.1.6
+      has-tostringtag: 1.0.0
+      is-async-function: 2.0.0
+      is-date-object: 1.0.5
+      is-finalizationregistry: 1.0.2
+      is-generator-function: 1.0.10
+      is-regex: 1.1.4
+      is-weakref: 1.0.2
+      isarray: 2.0.5
+      which-boxed-primitive: 1.0.2
+      which-collection: 1.0.1
+      which-typed-array: 1.1.11
+    dev: true
+
+  /which-collection@1.0.1:
+    resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==}
+    dependencies:
+      is-map: 2.0.2
+      is-set: 2.0.2
+      is-weakmap: 2.0.1
+      is-weakset: 2.0.2
+    dev: true
+
+  /which-typed-array@1.1.11:
+    resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==}
     engines: {node: '>= 0.4'}
     dependencies:
       available-typed-arrays: 1.0.5
@@ -4024,7 +4493,6 @@ packages:
       for-each: 0.3.3
       gopd: 1.0.1
       has-tostringtag: 1.0.0
-      is-typed-array: 1.1.10
     dev: true
 
   /which@2.0.2:
@@ -4055,7 +4523,7 @@ packages:
   /xregexp@5.1.1:
     resolution: {integrity: sha512-fKXeVorD+CzWvFs7VBuKTYIW63YD1e1osxwQ8caZ6o1jg6pDAbABDG54LCIq0j5cy7PjRvGIq6sef9DYPXpncg==}
     dependencies:
-      '@babel/runtime-corejs3': 7.22.6
+      '@babel/runtime-corejs3': 7.22.15
     dev: false
 
   /xxhash-wasm@0.4.2:
diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx
index 97cad452..8b0a4219 100644
--- a/FrontEnd/src/components/dialog/ConfirmDialog.tsx
+++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx
@@ -25,27 +25,24 @@ export default function ConfirmDialog({
       <DialogContainer
         title={title}
         titleColor={color ?? "danger"}
-        buttons={[
+        buttonsV2={[
           {
             key: "cancel",
             type: "normal",
-            props: {
-              text: "operationDialog.cancel",
-              color: "secondary",
-              outline: true,
-              onClick: closeDialog,
-            },
+            action: "minor",
+
+            text: "operationDialog.cancel",
+            onClick: closeDialog,
           },
           {
             key: "confirm",
             type: "normal",
-            props: {
-              text: "operationDialog.confirm",
-              color: "danger",
-              onClick: () => {
-                onConfirm();
-                closeDialog();
-              },
+            action: "major",
+            text: "operationDialog.confirm",
+            color: "danger",
+            onClick: () => {
+              onConfirm();
+              closeDialog();
             },
           },
         ]}
diff --git a/FrontEnd/src/components/dialog/DialogContainer.css b/FrontEnd/src/components/dialog/DialogContainer.css
index b3c52511..f0d27a66 100644
--- a/FrontEnd/src/components/dialog/DialogContainer.css
+++ b/FrontEnd/src/components/dialog/DialogContainer.css
@@ -7,6 +7,7 @@
 
 .cru-dialog-container-hr {
   margin: 1em 0;
+  border-color: var(--cru-text-minor-color);
 }
 
 .cru-dialog-container-button-row {
@@ -16,4 +17,4 @@
 
 .cru-dialog-container-button {
   margin-left: 1em;
-}
\ No newline at end of file
+}
diff --git a/FrontEnd/src/pages/timeline/Timeline.css b/FrontEnd/src/pages/timeline/Timeline.css
index 5bf0a7bb..01a6acd6 100644
--- a/FrontEnd/src/pages/timeline/Timeline.css
+++ b/FrontEnd/src/pages/timeline/Timeline.css
@@ -1,17 +1,19 @@
 .timeline-container {
+  --timeline-background-color: hsl(0, 0%, 95%);
   --timeline-shadow-color: hsla(0, 0%, 0%, 0.5);
   --timeline-card-shadow: 2px 1px 10px -2px var(--timeline-shadow-color);
-  --timeline-post-line-color: hsl(56, 82%, 55%);
-  --timeline-post-line-shadow: 2px 1px 10px -1px var(--timeline-shadow-color);
   --timeline-post-card-background-color: hsl(0, 0%, 100%);
-  --timeline-post-card-shadow: 4px 2px 10px -2px var(--timeline-shadow-color);
+  --timeline-post-card-shadow: 0px 0px 11px -2px var(--timeline-shadow-color);
   --timeline-post-card-border-radius: 10px;
   --timeline-post-text-color: hsl(0, 0%, 0%);
+  --timeline-datetime-label-background-color: hsl(0, 0%, 30%);
 }
 
 @media (prefers-color-scheme: dark) {
-  .timeline {
-    --timeline-post-card-background-color: hsl(0, 0%, 12%);
+  .timeline-container {
+    --timeline-background-color: hsl(0, 0%, 0%);
+    --timeline-post-card-background-color: hsl(0, 0%, 15%);
+    --timeline-post-card-shadow: none;
   }
 }
 
@@ -20,11 +22,7 @@
   position: relative;
   width: 100%;
   padding-top: 10px;
-}
-
-.timeline-content-image {
-  max-width: 80%;
-  max-height: 200px;
+  background: var(--timeline-background-color);
 }
 
 .timeline-post-item-options-mask {
@@ -58,4 +56,3 @@
   vertical-align: middle;
   margin-right: 0.6em;
 }
-
diff --git a/FrontEnd/src/pages/timeline/TimelineDateLabel.css b/FrontEnd/src/pages/timeline/TimelineDateLabel.css
index 1c66045b..47a4cb44 100644
--- a/FrontEnd/src/pages/timeline/TimelineDateLabel.css
+++ b/FrontEnd/src/pages/timeline/TimelineDateLabel.css
@@ -1,9 +1,9 @@
 .timeline-post-date-badge {
   display: inline-block;
-  padding: 0.1em 0.4em;
+  padding: 0.2em 0.5em;
   border-radius: 0.4em;
-  background: #7c7c7c;
+  background: var(--timeline-datetime-label-background-color);
   color: white;
   font-size: 0.8em;
   margin-left: 5em;
-}
\ No newline at end of file
+}
diff --git a/FrontEnd/src/pages/timeline/TimelinePostList.tsx b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
index 7912260a..66262ccd 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostList.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
@@ -53,7 +53,6 @@ export default function TimelinePostList(props: TimelinePostListViewProps) {
 
   return (
     <div>
-      <div className="timeline-post-timeline" />
       {groupedPosts.map((group) => {
         return (
           <Fragment key={group.date.toDateString()}>
-- 
cgit v1.2.3

