From 0e183074b326cf04a23ae1f1ba8dcc56166df485 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Thu, 20 Jul 2023 20:44:15 +0800
Subject: ...

---
 FrontEnd/src/pages/about/index.tsx | 86 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 86 insertions(+)
 create mode 100644 FrontEnd/src/pages/about/index.tsx

(limited to 'FrontEnd/src/pages/about/index.tsx')

diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx
new file mode 100644
index 00000000..afd4de34
--- /dev/null
+++ b/FrontEnd/src/pages/about/index.tsx
@@ -0,0 +1,86 @@
+import "./index.css";
+
+import { useC } from "@/common";
+
+interface Credit {
+  name: string;
+  url: string;
+}
+
+type Credits = Credit[];
+
+const frontendCredits: Credits = [
+  {
+    name: "react.js",
+    url: "https://reactjs.org",
+  },
+  {
+    name: "typescript",
+    url: "https://www.typescriptlang.org",
+  },
+  {
+    name: "bootstrap",
+    url: "https://getbootstrap.com",
+  },
+  {
+    name: "parcel.js",
+    url: "https://parceljs.org",
+  },
+  {
+    name: "eslint",
+    url: "https://eslint.org",
+  },
+  {
+    name: "prettier",
+    url: "https://prettier.io",
+  },
+];
+
+const backendCredits: Credits = [
+  {
+    name: "ASP.NET Core",
+    url: "https://dotnet.microsoft.com/learn/aspnet/what-is-aspnet-core",
+  },
+  { name: "sqlite", url: "https://sqlite.org" },
+  {
+    name: "ImageSharp",
+    url: "https://github.com/SixLabors/ImageSharp",
+  },
+];
+
+export default function AboutPage() {
+  const c = useC();
+
+  return (
+    <div className="about-page">
+      <h2>{c("about.credits.title")}</h2>
+      <p>{c("about.credits.content")}</p>
+      <h3>{c("about.credits.frontend")}</h3>
+      <ul>
+        {frontendCredits.map((item, index) => {
+          return (
+            <li key={index}>
+              <a href={item.url} target="_blank" rel="noopener noreferrer">
+                {item.name}
+              </a>
+            </li>
+          );
+        })}
+        <li>...</li>
+      </ul>
+      <h3>{c("about.credits.backend")}</h3>
+      <ul>
+        {backendCredits.map((item, index) => {
+          return (
+            <li key={index}>
+              <a href={item.url} target="_blank" rel="noopener noreferrer">
+                {item.name}
+              </a>
+            </li>
+          );
+        })}
+        <li>...</li>
+      </ul>
+    </div>
+  );
+}
-- 
cgit v1.2.3


From d7b050ef7f047f841ffcda96fbdc9857574e97b9 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Fri, 21 Jul 2023 17:02:05 +0800
Subject: ...

---
 FrontEnd/src/App.tsx                               |   8 +-
 FrontEnd/src/index.css                             |   1 -
 FrontEnd/src/pages/about/index.css                 |   1 -
 FrontEnd/src/pages/about/index.tsx                 |   5 +-
 FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx  |   6 +-
 .../src/pages/setting/ChangeNicknameDialog.tsx     |   2 +-
 .../src/pages/setting/ChangePasswordDialog.tsx     |   2 +-
 FrontEnd/src/pages/setting/index.css               |  54 +++-
 FrontEnd/src/pages/setting/index.tsx               | 171 +++++-----
 FrontEnd/src/views/common/Card.css                 |   8 +-
 FrontEnd/src/views/common/Card.tsx                 |   8 +-
 FrontEnd/src/views/common/Page.tsx                 |  15 +
 FrontEnd/src/views/common/index.css                |   8 +
 FrontEnd/src/views/common/theme.css                |   5 +-
 FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 354 ---------------------
 .../src/views/settings/ChangeNicknameDialog.tsx    |  34 --
 .../src/views/settings/ChangePasswordDialog.tsx    |  69 ----
 FrontEnd/src/views/settings/index.css              |  31 --
 FrontEnd/src/views/settings/index.tsx              | 338 --------------------
 19 files changed, 182 insertions(+), 938 deletions(-)
 create mode 100644 FrontEnd/src/views/common/Page.tsx
 delete mode 100644 FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
 delete mode 100644 FrontEnd/src/views/settings/ChangeNicknameDialog.tsx
 delete mode 100644 FrontEnd/src/views/settings/ChangePasswordDialog.tsx
 delete mode 100644 FrontEnd/src/views/settings/index.css
 delete mode 100644 FrontEnd/src/views/settings/index.tsx

(limited to 'FrontEnd/src/pages/about/index.tsx')

diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx
index 07a8780f..92fe0652 100644
--- a/FrontEnd/src/App.tsx
+++ b/FrontEnd/src/App.tsx
@@ -4,11 +4,11 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
 import AppBar from "./views/common/AppBar";
 import NotFoundPage from "./pages/404";
 import LoadingPage from "./views/common/LoadingPage";
-import About from "./pages/about";
+import AboutPage from "./pages/about";
+import SettingPage from "./pages/setting";
 import Center from "./views/center";
 import Login from "./views/login";
 import Register from "./views/register";
-import Settings from "./views/settings";
 import TimelinePage from "./views/timeline";
 import Search from "./views/search";
 import Admin from "./views/admin";
@@ -24,8 +24,8 @@ export default function App() {
           <Route path="center" element={<Center />} />
           <Route path="login" element={<Login />} />
           <Route path="register" element={<Register />} />
-          <Route path="settings" element={<Settings />} />
-          <Route path="about" element={<About />} />
+          <Route path="settings" element={<SettingPage />} />
+          <Route path="about" element={<AboutPage />} />
           <Route path="search" element={<Search />} />
           <Route path="admin/*" element={<Admin />} />
           <Route path=":owner" element={<TimelinePage />} />
diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css
index 3478db05..49791c23 100644
--- a/FrontEnd/src/index.css
+++ b/FrontEnd/src/index.css
@@ -1,4 +1,3 @@
-@import "npm:bootstrap/dist/css/bootstrap-grid.css";
 @import "npm:bootstrap-icons/font/bootstrap-icons.css";
 
 @import "./views/common/index.css";
diff --git a/FrontEnd/src/pages/about/index.css b/FrontEnd/src/pages/about/index.css
index 487f4a0a..1ce7a7c8 100644
--- a/FrontEnd/src/pages/about/index.css
+++ b/FrontEnd/src/pages/about/index.css
@@ -1,5 +1,4 @@
 .about-page {
-  padding: 1em 2em;
   line-height: 1.5;
 }
 
diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx
index afd4de34..acec1735 100644
--- a/FrontEnd/src/pages/about/index.tsx
+++ b/FrontEnd/src/pages/about/index.tsx
@@ -1,6 +1,7 @@
 import "./index.css";
 
 import { useC } from "@/common";
+import Page from "@/views/common/Page";
 
 interface Credit {
   name: string;
@@ -52,7 +53,7 @@ export default function AboutPage() {
   const c = useC();
 
   return (
-    <div className="about-page">
+    <Page className="about-page">
       <h2>{c("about.credits.title")}</h2>
       <p>{c("about.credits.content")}</p>
       <h3>{c("about.credits.frontend")}</h3>
@@ -81,6 +82,6 @@ export default function AboutPage() {
         })}
         <li>...</li>
       </ul>
-    </div>
+    </Page>
   );
 }
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
index 44bd2c68..b2a4e2a8 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
@@ -9,9 +9,9 @@ import { useUser } from "@/services/user";
 
 import { getHttpUserClient } from "@/http/user";
 
-import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper";
-import Button from "../common/button/Button";
-import Dialog from "../common/dialog/Dialog";
+import ImageCropper, { Clip, applyClipToImage } from "@/views/common/ImageCropper";
+import Button from "@/views/common/button/Button";
+import Dialog from "@/views/common/dialog/Dialog";
 
 export interface ChangeAvatarDialogProps {
   open: boolean;
diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
index 7ba12de8..11c86222 100644
--- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
@@ -2,7 +2,7 @@ import { getHttpUserClient } from "@/http/user";
 import { useUser } from "@/services/user";
 import * as React from "react";
 
-import OperationDialog from "../common/dialog/OperationDialog";
+import OperationDialog from "@/views/common/dialog/OperationDialog";
 
 export interface ChangeNicknameDialogProps {
   open: boolean;
diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
index a34ca4a7..a523b454 100644
--- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
@@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
 
 import { userService } from "@/services/user";
 
-import OperationDialog from "../common/dialog/OperationDialog";
+import OperationDialog from "@/views/common/dialog/OperationDialog";
 
 export interface ChangePasswordDialogProps {
   open: boolean;
diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css
index ccf7a97a..af5ccf20 100644
--- a/FrontEnd/src/pages/setting/index.css
+++ b/FrontEnd/src/pages/setting/index.css
@@ -1,3 +1,5 @@
+/* TODO: Make item prettier. */
+
 .change-avatar-cropper-row {
   max-height: 400px;
 }
@@ -8,20 +10,56 @@
   max-height: 400px;
 }
 
-.settings-item {
+.setting-section {
+  padding: 1em 0;
+  margin: 1em 0;
+}
+
+.setting-section-title {
+  padding: 0 1em;
+}
+
+.setting-section-item-area {
+  margin-top: 1em;
+  border-top: 1px solid var(--cru-key-container-color);
+}
+
+.setting-item-container {
   padding: 0.5em 1em;
-  transition: background 0.3s;
-  border-bottom: 1px solid #e9ecef;
+  transition: background-color 0.3s;
+  color: var(--cru-surface-on-color);
+  background-color: var(--cru-surface-color);
+  border-bottom: 1px solid var(--cru-key-container-color);
+  display: flex;
   align-items: center;
 }
-.settings-item.first {
-  border-top: 1px solid #e9ecef;
+
+.setting-item-container:hover {
+  background-color: var(--cru-key-container-1-color);
+  border-bottom-color: var(--cru-key-container-1-color);
+}
+
+.setting-item-label-sub {
+  color: var(--cru-secondary-text-color);
 }
-.settings-item.clickable {
+
+.setting-item-container.setting-type-button {
   cursor: pointer;
 }
-.settings-item:hover {
-  background: #dee2e6;
+
+.setting-item-container.setting-type-button.danger {
+  color: var(--cru-danger-color);
+}
+
+
+@media (max-width: 576) {
+  .setting-item-container.setting-type-select {
+    flex-direction: column;
+  }
+
+  .setting-item-container.setting-type-select .setting-item-value-area {
+    margin-top: 1em;
+  }
 }
 
 .register-code {
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx
index 4e28585e..5d6dcbc0 100644
--- a/FrontEnd/src/pages/setting/index.tsx
+++ b/FrontEnd/src/pages/setting/index.tsx
@@ -11,11 +11,11 @@ import classNames from "classnames";
 import { useC, Text } from "@/common";
 import { useUser, userService } from "@/services/user";
 import { getHttpUserClient } from "@/http/user";
-import { TimelineVisibility } from "@/http/timeline";
 
 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 ChangePasswordDialog from "./ChangePasswordDialog";
 import ChangeAvatarDialog from "./ChangeAvatarDialog";
 import ChangeNicknameDialog from "./ChangeNicknameDialog";
@@ -24,9 +24,9 @@ import "./index.css";
 import { pushAlert } from "@/services/alert";
 
 interface SettingSectionProps
-  extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
+  extends Omit<ComponentPropsWithoutRef<typeof Card>, "title"> {
   title: Text;
-  children: ReactNode;
+  children?: ReactNode;
 }
 
 function SettingSection({
@@ -40,7 +40,7 @@ function SettingSection({
   return (
     <Card className={classNames(className, "setting-section")} {...otherProps}>
       <h2 className="setting-section-title">{c(title)}</h2>
-      {children}
+      <div className="setting-section-item-area">{children}</div>
     </Card>
   );
 }
@@ -115,6 +115,7 @@ function SelectSettingsItem({
         <Spinner />
       ) : (
         <select
+          className="select-setting-item-select"
           value={value}
           onChange={(e) => {
             onSelect(e.target.value);
@@ -131,16 +132,14 @@ function SelectSettingsItem({
   );
 }
 
-function RegisterCodeSettingItem({
-  openRenewDialog,
-}: {
-  openRenewDialog: () => void;
-}) {
+function RegisterCodeSettingItem() {
   const user = useUser();
 
   // undefined: loading
   const [registerCode, setRegisterCode] = useState<undefined | null | string>();
 
+  const [dialogOpen, setDialogOpen] = useState(false);
+
   useEffect(() => {
     setRegisterCode(undefined);
   }, [user]);
@@ -159,7 +158,7 @@ function RegisterCodeSettingItem({
     <SettingItemContainer
       title="settings.myRegisterCode"
       description="settings.myRegisterCodeDesc"
-      onClick={openRenewDialog}
+      onClick={() => setDialogOpen(true)}
     >
       {registerCode === undefined ? (
         <Spinner />
@@ -181,13 +180,59 @@ function RegisterCodeSettingItem({
           {registerCode}
         </code>
       )}
+
+      <ConfirmDialog
+        title="settings.renewRegisterCode"
+        body="settings.renewRegisterCodeDesc"
+        onClose={() => setDialogOpen(false)}
+        open={dialogOpen}
+        onConfirm={() => {
+          if (user == null) throw new Error();
+          void getHttpUserClient()
+            .renewRegisterCode(user.username)
+            .then(() => {
+              setRegisterCode(undefined);
+            });
+        }}
+      />
     </SettingItemContainer>
   );
 }
 
-export default function SettingsPage() {
-  const c = useC();
+function LanguageChangeSettingItem() {
   const { i18n } = useTranslation();
+
+  const language = i18n.language.slice(0, 2);
+
+  return (
+    <SelectSettingsItem
+      title="settings.languagePrimary"
+      description="settings.languageSecondary"
+      options={[
+        {
+          value: "zh",
+          label: {
+            type: "custom",
+            value: "中文",
+          },
+        },
+        {
+          value: "en",
+          label: {
+            type: "custom",
+            value: "English",
+          },
+        },
+      ]}
+      value={language}
+      onSelect={(value) => {
+        void i18n.changeLanguage(value);
+      }}
+    />
+  );
+}
+
+export default function SettingPage() {
   const user = useUser();
   const navigate = useNavigate();
 
@@ -204,66 +249,36 @@ export default function SettingsPage() {
     return () => setDialog(name);
   }
 
-  const language = i18n.language.slice(0, 2);
-
   return (
-    <>
-      <div className="container">
-        {user ? (
-          <SettingSection title="settings.subheader.account">
-            <RegisterCodeSettingItem
-              openRenewDialog={dialogOpener("renew-register-code")}
-            />
-            <ButtonSettingItem
-              title="settings.changeAvatar"
-              onClick={dialogOpener("change-avatar")}
-            />
-            <ButtonSettingItem
-              title="settings.changeNickname"
-              onClick={dialogOpener("change-nickname")}
-            />
-            <ButtonSettingItem
-              title="settings.changePassword"
-              onClick={dialogOpener("change-password")}
-              danger
-            />
-            <ButtonSettingItem
-              title="settings.logout"
-              onClick={dialogOpener("logout")}
-              danger
-            />
-          </SettingSection>
-        ) : null}
-        <SettingSection title="settings.subheaders.customization">
-          <SelectSettingsItem
-            title="settings.languagePrimary"
-            subtext="settings.languageSecondary"
-            options={[
-              {
-                value: "zh",
-                label: {
-                  type: "custom",
-                  value: "中文",
-                },
-              },
-              {
-                value: "en",
-                label: {
-                  type: "custom",
-                  value: "English",
-                },
-              },
-            ]}
-            value={language}
-            onSelect={(value) => {
-              void i18n.changeLanguage(value);
-            }}
-            first
+    <Page noTopPadding>
+      {user ? (
+        <SettingSection title="settings.subheader.account">
+          <RegisterCodeSettingItem />
+          <ButtonSettingItem
+            title="settings.changeAvatar"
+            onClick={dialogOpener("change-avatar")}
+          />
+          <ButtonSettingItem
+            title="settings.changeNickname"
+            onClick={dialogOpener("change-nickname")}
+          />
+          <ButtonSettingItem
+            title="settings.changePassword"
+            onClick={dialogOpener("change-password")}
+            danger
+          />
+          <ButtonSettingItem
+            title="settings.logout"
+            onClick={dialogOpener("logout")}
+            danger
           />
         </SettingSection>
-      </div>
+      ) : null}
+      <SettingSection title="settings.subheader.customization">
+        <LanguageChangeSettingItem />
+      </SettingSection>
       <ChangePasswordDialog
-        open={dialog === "changepassword"}
+        open={dialog === "change-password"}
         close={() => setDialog(null)}
       />
       <ConfirmDialog
@@ -277,28 +292,14 @@ export default function SettingsPage() {
           });
         }}
       />
-      <ConfirmDialog
-        title="settings.renewRegisterCode"
-        body="settings.renewRegisterCodeDesc"
-        onClose={() => setDialog(null)}
-        open={dialog === "renewregistercode"}
-        onConfirm={() => {
-          if (user == null) throw new UiLogicError();
-          void getHttpUserClient()
-            .renewRegisterCode(user.username)
-            .then(() => {
-              setRegisterCode(undefined);
-            });
-        }}
-      />
       <ChangeAvatarDialog
-        open={dialog === "changeavatar"}
+        open={dialog === "change-avatar"}
         close={() => setDialog(null)}
       />
       <ChangeNicknameDialog
-        open={dialog === "changenickname"}
+        open={dialog === "change-nickname"}
         close={() => setDialog(null)}
       />
-    </>
+    </Page>
   );
 }
diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css
index 98cb4cdd..5b3dbbe9 100644
--- a/FrontEnd/src/views/common/Card.css
+++ b/FrontEnd/src/views/common/Card.css
@@ -1,9 +1,11 @@
 .cru-card {
+  border: solid 1px;
   border-radius: var(--cru-card-border-radius);
-  background-color: var(--cru-primary-container-color);
+  background-color: var(--cru-key-container-color);
+  border-color: var(--cru-key-container-color);
   transition: all 0.3s;
 }
 
 .cru-card:hover {
-  border-color: var(--cru-primary-1-color);
-}
+  border-color: var(--cru-key-1-color);
+}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx
index 5ff89b61..35e605af 100644
--- a/FrontEnd/src/views/common/Card.tsx
+++ b/FrontEnd/src/views/common/Card.tsx
@@ -1,22 +1,26 @@
 import { ComponentPropsWithoutRef, Ref } from "react";
 import classNames from "classnames";
 
+import { ThemeColor } from "./common";
 import "./Card.css";
 
 interface CardProps extends ComponentPropsWithoutRef<"div"> {
-  containerRef?: Ref<HTMLDivElement> | null;
+  containerRef?: Ref<HTMLDivElement>;
+  color?: ThemeColor;
 }
 
 export default function Card({
+  color,
   className,
   children,
   containerRef,
   ...otherProps
 }: CardProps) {
+  color = color ?? "primary";
   return (
     <div
       ref={containerRef}
-      className={classNames("cru-card", className)}
+      className={classNames("cru-card", `cru-${color}`, className)}
       {...otherProps}
     >
       {children}
diff --git a/FrontEnd/src/views/common/Page.tsx b/FrontEnd/src/views/common/Page.tsx
new file mode 100644
index 00000000..86fdb2f5
--- /dev/null
+++ b/FrontEnd/src/views/common/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/views/common/index.css b/FrontEnd/src/views/common/index.css
index eb82c4bf..1f9b5086 100644
--- a/FrontEnd/src/views/common/index.css
+++ b/FrontEnd/src/views/common/index.css
@@ -13,6 +13,14 @@ body {
   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;
 }
diff --git a/FrontEnd/src/views/common/theme.css b/FrontEnd/src/views/common/theme.css
index 3ad45996..6478ed7a 100644
--- a/FrontEnd/src/views/common/theme.css
+++ b/FrontEnd/src/views/common/theme.css
@@ -2,5 +2,8 @@
 
 :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-card-border-radius: 4px;
-}
\ No newline at end of file
+
+  --cru-secondary-text-color: var(--cru-surface-on-color);
+}
diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
deleted file mode 100644
index 44bd2c68..00000000
--- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
+++ /dev/null
@@ -1,354 +0,0 @@
-import { useState, useEffect } from "react";
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-import { AxiosError } from "axios";
-
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-
-import { useUser } from "@/services/user";
-
-import { getHttpUserClient } from "@/http/user";
-
-import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper";
-import Button from "../common/button/Button";
-import Dialog from "../common/dialog/Dialog";
-
-export interface ChangeAvatarDialogProps {
-  open: boolean;
-  close: () => void;
-}
-
-const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
-  const { t } = useTranslation();
-
-  const user = useUser();
-
-  const [file, setFile] = React.useState<File | null>(null);
-  const [fileUrl, setFileUrl] = React.useState<string | null>(null);
-  const [clip, setClip] = React.useState<Clip | null>(null);
-  const [cropImgElement, setCropImgElement] =
-    React.useState<HTMLImageElement | null>(null);
-  const [resultBlob, setResultBlob] = React.useState<Blob | null>(null);
-  const [resultUrl, setResultUrl] = React.useState<string | null>(null);
-
-  const [state, setState] = React.useState<
-    | "select"
-    | "crop"
-    | "processcrop"
-    | "preview"
-    | "uploading"
-    | "success"
-    | "error"
-  >("select");
-
-  const [message, setMessage] = useState<I18nText>(
-    "settings.dialogChangeAvatar.prompt.select"
-  );
-
-  const trueMessage = convertI18nText(message, t);
-
-  const closeDialog = props.close;
-
-  const close = React.useCallback((): void => {
-    if (!(state === "uploading")) {
-      closeDialog();
-    }
-  }, [state, closeDialog]);
-
-  useEffect(() => {
-    if (file != null) {
-      const url = URL.createObjectURL(file);
-      setClip(null);
-      setFileUrl(url);
-      setState("crop");
-      return () => {
-        URL.revokeObjectURL(url);
-      };
-    } else {
-      setFileUrl(null);
-      setState("select");
-    }
-  }, [file]);
-
-  React.useEffect(() => {
-    if (resultBlob != null) {
-      const url = URL.createObjectURL(resultBlob);
-      setResultUrl(url);
-      setState("preview");
-      return () => {
-        URL.revokeObjectURL(url);
-      };
-    } else {
-      setResultUrl(null);
-    }
-  }, [resultBlob]);
-
-  const onSelectFile = React.useCallback(
-    (e: React.ChangeEvent<HTMLInputElement>): void => {
-      const files = e.target.files;
-      if (files == null || files.length === 0) {
-        setFile(null);
-      } else {
-        setFile(files[0]);
-      }
-    },
-    []
-  );
-
-  const onCropNext = React.useCallback(() => {
-    if (
-      cropImgElement == null ||
-      clip == null ||
-      clip.width === 0 ||
-      file == null
-    ) {
-      throw new UiLogicError();
-    }
-
-    setState("processcrop");
-    void applyClipToImage(cropImgElement, clip, file.type).then((b) => {
-      setResultBlob(b);
-    });
-  }, [cropImgElement, clip, file]);
-
-  const onCropPrevious = React.useCallback(() => {
-    setFile(null);
-    setState("select");
-  }, []);
-
-  const onPreviewPrevious = React.useCallback(() => {
-    setResultBlob(null);
-    setState("crop");
-  }, []);
-
-  const upload = React.useCallback(() => {
-    if (resultBlob == null) {
-      throw new UiLogicError();
-    }
-
-    if (user == null) {
-      throw new UiLogicError();
-    }
-
-    setState("uploading");
-    getHttpUserClient()
-      .putAvatar(user.username, resultBlob)
-      .then(
-        () => {
-          setState("success");
-        },
-        (e: unknown) => {
-          setState("error");
-          setMessage({ type: "custom", value: (e as AxiosError).message });
-        }
-      );
-  }, [user, resultBlob]);
-
-  const createPreviewRow = (): React.ReactElement => {
-    if (resultUrl == null) {
-      throw new UiLogicError();
-    }
-    return (
-      <div className="row justify-content-center">
-        <div className="col col-auto">
-          <img
-            className="change-avatar-img"
-            src={resultUrl}
-            alt={t("settings.dialogChangeAvatar.previewImgAlt") ?? undefined}
-          />
-        </div>
-      </div>
-    );
-  };
-
-  return (
-    <Dialog open={props.open} onClose={close}>
-      <h3 className="cru-color-primary">
-        {t("settings.dialogChangeAvatar.title")}
-      </h3>
-      <hr />
-      {(() => {
-        if (state === "select") {
-          return (
-            <>
-              <div className="container">
-                <div className="row">
-                  {t("settings.dialogChangeAvatar.prompt.select")}
-                </div>
-                <div className="row">
-                  <input
-                    className="px-0"
-                    type="file"
-                    accept="image/*"
-                    onChange={onSelectFile}
-                  />
-                </div>
-              </div>
-              <hr />
-              <div className="cru-dialog-bottom-area">
-                <Button
-                  text="operationDialog.cancel"
-                  color="secondary"
-                  onClick={close}
-                />
-              </div>
-            </>
-          );
-        } else if (state === "crop") {
-          if (fileUrl == null) {
-            throw new UiLogicError();
-          }
-          return (
-            <>
-              <div className="container">
-                <div className="row justify-content-center">
-                  <ImageCropper
-                    clip={clip}
-                    onChange={setClip}
-                    imageUrl={fileUrl}
-                    imageElementCallback={setCropImgElement}
-                  />
-                </div>
-                <div className="row">
-                  {t("settings.dialogChangeAvatar.prompt.crop")}
-                </div>
-              </div>
-              <hr />
-              <div className="cru-dialog-bottom-area">
-                <Button
-                  text="operationDialog.cancel"
-                  color="secondary"
-                  outline
-                  onClick={close}
-                />
-                <Button
-                  text="operationDialog.previousStep"
-                  color="secondary"
-                  outline
-                  onClick={onCropPrevious}
-                />
-                <Button
-                  text="operationDialog.nextStep"
-                  color="primary"
-                  onClick={onCropNext}
-                  disabled={
-                    cropImgElement == null || clip == null || clip.width === 0
-                  }
-                />
-              </div>
-            </>
-          );
-        } else if (state === "processcrop") {
-          return (
-            <>
-              <div className="container">
-                <div className="row">
-                  {t("settings.dialogChangeAvatar.prompt.processingCrop")}
-                </div>
-              </div>
-              <hr />
-              <div className="cru-dialog-bottom-area">
-                <Button
-                  text="operationDialog.cancel"
-                  color="secondary"
-                  onClick={close}
-                  outline
-                />
-                <Button
-                  text="operationDialog.previousStep"
-                  color="secondary"
-                  onClick={onPreviewPrevious}
-                  outline
-                />
-              </div>
-            </>
-          );
-        } else if (state === "preview") {
-          return (
-            <>
-              <div className="container">
-                {createPreviewRow()}
-                <div className="row">
-                  {t("settings.dialogChangeAvatar.prompt.preview")}
-                </div>
-              </div>
-              <hr />
-              <div className="cru-dialog-bottom-area">
-                <Button
-                  text="operationDialog.cancel"
-                  color="secondary"
-                  outline
-                  onClick={close}
-                />
-                <Button
-                  text="operationDialog.previousStep"
-                  color="secondary"
-                  outline
-                  onClick={onPreviewPrevious}
-                />
-                <Button
-                  text="settings.dialogChangeAvatar.upload"
-                  color="primary"
-                  onClick={upload}
-                />
-              </div>
-            </>
-          );
-        } else if (state === "uploading") {
-          return (
-            <>
-              <div className="container">
-                {createPreviewRow()}
-                <div className="row">
-                  {t("settings.dialogChangeAvatar.prompt.uploading")}
-                </div>
-              </div>
-            </>
-          );
-        } else if (state === "success") {
-          return (
-            <>
-              <div className="container">
-                <div className="row p-4 text-success">
-                  {t("operationDialog.success")}
-                </div>
-              </div>
-              <hr />
-              <div className="cru-dialog-bottom-area">
-                <Button
-                  text="operationDialog.ok"
-                  color="success"
-                  onClick={close}
-                />
-              </div>
-            </>
-          );
-        } else {
-          return (
-            <>
-              <div className="container">
-                {createPreviewRow()}
-                <div className="row text-danger">{trueMessage}</div>
-              </div>
-              <hr />
-              <div>
-                <Button
-                  text="operationDialog.cancel"
-                  color="secondary"
-                  onClick={close}
-                />
-                <Button
-                  text="operationDialog.retry"
-                  color="primary"
-                  onClick={upload}
-                />
-              </div>
-            </>
-          );
-        }
-      })()}
-    </Dialog>
-  );
-};
-
-export default ChangeAvatarDialog;
diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx
deleted file mode 100644
index 7ba12de8..00000000
--- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { getHttpUserClient } from "@/http/user";
-import { useUser } from "@/services/user";
-import * as React from "react";
-
-import OperationDialog from "../common/dialog/OperationDialog";
-
-export interface ChangeNicknameDialogProps {
-  open: boolean;
-  close: () => void;
-}
-
-const ChangeNicknameDialog: React.FC<ChangeNicknameDialogProps> = (props) => {
-  const user = useUser();
-
-  if (user == null) return null;
-
-  return (
-    <OperationDialog
-      open={props.open}
-      title="settings.dialogChangeNickname.title"
-      inputScheme={[
-        { type: "text", label: "settings.dialogChangeNickname.inputLabel" },
-      ]}
-      onProcess={([newNickname]) => {
-        return getHttpUserClient().patch(user.username, {
-          nickname: newNickname,
-        });
-      }}
-      onClose={props.close}
-    />
-  );
-};
-
-export default ChangeNicknameDialog;
diff --git a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx b/FrontEnd/src/views/settings/ChangePasswordDialog.tsx
deleted file mode 100644
index a34ca4a7..00000000
--- a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useState } from "react";
-import * as React from "react";
-import { useNavigate } from "react-router-dom";
-
-import { userService } from "@/services/user";
-
-import OperationDialog from "../common/dialog/OperationDialog";
-
-export interface ChangePasswordDialogProps {
-  open: boolean;
-  close: () => void;
-}
-
-const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => {
-  const navigate = useNavigate();
-
-  const [redirect, setRedirect] = useState<boolean>(false);
-
-  return (
-    <OperationDialog
-      open={props.open}
-      title="settings.dialogChangePassword.title"
-      themeColor="danger"
-      inputPrompt="settings.dialogChangePassword.prompt"
-      inputScheme={[
-        {
-          type: "text",
-          label: "settings.dialogChangePassword.inputOldPassword",
-          password: true,
-        },
-        {
-          type: "text",
-          label: "settings.dialogChangePassword.inputNewPassword",
-          password: true,
-        },
-        {
-          type: "text",
-          label: "settings.dialogChangePassword.inputRetypeNewPassword",
-          password: true,
-        },
-      ]}
-      inputValidator={([oldPassword, newPassword, retypedNewPassword]) => {
-        const result: Record<number, string> = {};
-        if (oldPassword === "") {
-          result[0] = "settings.dialogChangePassword.errorEmptyOldPassword";
-        }
-        if (newPassword === "") {
-          result[1] = "settings.dialogChangePassword.errorEmptyNewPassword";
-        }
-        if (retypedNewPassword !== newPassword) {
-          result[2] = "settings.dialogChangePassword.errorRetypeNotMatch";
-        }
-        return result;
-      }}
-      onProcess={async ([oldPassword, newPassword]) => {
-        await userService.changePassword(oldPassword, newPassword);
-        setRedirect(true);
-      }}
-      onClose={() => {
-        props.close();
-        if (redirect) {
-          navigate("/login");
-        }
-      }}
-    />
-  );
-};
-
-export default ChangePasswordDialog;
diff --git a/FrontEnd/src/views/settings/index.css b/FrontEnd/src/views/settings/index.css
deleted file mode 100644
index ccf7a97a..00000000
--- a/FrontEnd/src/views/settings/index.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.change-avatar-cropper-row {
-  max-height: 400px;
-}
-
-.change-avatar-img {
-  min-width: 50%;
-  max-width: 100%;
-  max-height: 400px;
-}
-
-.settings-item {
-  padding: 0.5em 1em;
-  transition: background 0.3s;
-  border-bottom: 1px solid #e9ecef;
-  align-items: center;
-}
-.settings-item.first {
-  border-top: 1px solid #e9ecef;
-}
-.settings-item.clickable {
-  cursor: pointer;
-}
-.settings-item:hover {
-  background: #dee2e6;
-}
-
-.register-code {
-  border: 1px solid black;
-  border-radius: 3px;
-  padding: 0.2em;
-}
\ No newline at end of file
diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx
deleted file mode 100644
index 6647826f..00000000
--- a/FrontEnd/src/views/settings/index.tsx
+++ /dev/null
@@ -1,338 +0,0 @@
-import { useState } from "react";
-import * as React from "react";
-import { useNavigate } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import classNames from "classnames";
-
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-import { useUser, userService } from "@/services/user";
-import { getHttpUserClient } from "@/http/user";
-import { TimelineVisibility } from "@/http/timeline";
-
-import ConfirmDialog from "../common/dialog/ConfirmDialog";
-import Card from "../common/Card";
-import Spinner from "../common/Spinner";
-import ChangePasswordDialog from "./ChangePasswordDialog";
-import ChangeAvatarDialog from "./ChangeAvatarDialog";
-import ChangeNicknameDialog from "./ChangeNicknameDialog";
-
-import "./index.css";
-import { pushAlert } from "@/services/alert";
-
-interface SettingSectionProps {
-  title: I18nText;
-  children: React.ReactNode;
-}
-
-const SettingSection: React.FC<SettingSectionProps> = ({ title, children }) => {
-  const { t } = useTranslation();
-
-  return (
-    <Card className="my-3 py-3">
-      <h3 className="px-3 mb-3 cru-color-primary">
-        {convertI18nText(title, t)}
-      </h3>
-      {children}
-    </Card>
-  );
-};
-
-interface SettingItemContainerWithoutChildrenProps {
-  title: I18nText;
-  subtext?: I18nText;
-  first?: boolean;
-  danger?: boolean;
-  style?: React.CSSProperties;
-  className?: string;
-  onClick?: () => void;
-}
-
-interface SettingItemContainerProps
-  extends SettingItemContainerWithoutChildrenProps {
-  children?: React.ReactNode;
-}
-
-function SettingItemContainer({
-  title,
-  subtext,
-  first,
-  danger,
-  children,
-  style,
-  className,
-  onClick,
-}: SettingItemContainerProps): JSX.Element {
-  const { t } = useTranslation();
-
-  return (
-    <div
-      style={style}
-      className={classNames(
-        "row settings-item mx-0",
-        first && "first",
-        onClick && "clickable",
-        className,
-      )}
-      onClick={onClick}
-    >
-      <div className="px-0 col col-auto">
-        <div className={classNames(danger && "cru-color-danger")}>
-          {convertI18nText(title, t)}
-        </div>
-        <small className="d-block cru-color-secondary">
-          {convertI18nText(subtext, t)}
-        </small>
-      </div>
-      <div className="col col-auto">{children}</div>
-    </div>
-  );
-}
-
-type ButtonSettingItemProps = SettingItemContainerWithoutChildrenProps;
-
-const ButtonSettingItem: React.FC<ButtonSettingItemProps> = ({ ...props }) => {
-  return <SettingItemContainer {...props} />;
-};
-
-interface SelectSettingItemProps
-  extends SettingItemContainerWithoutChildrenProps {
-  options: {
-    value: string;
-    label: I18nText;
-  }[];
-  value?: string;
-  onSelect: (value: string) => void;
-}
-
-const SelectSettingsItem: React.FC<SelectSettingItemProps> = ({
-  options,
-  value,
-  onSelect,
-  ...props
-}) => {
-  const { t } = useTranslation();
-
-  return (
-    <SettingItemContainer {...props}>
-      {value == null ? (
-        <Spinner />
-      ) : (
-        <select
-          value={value}
-          onChange={(e) => {
-            onSelect(e.target.value);
-          }}
-        >
-          {options.map(({ value, label }) => (
-            <option key={value} value={value}>
-              {convertI18nText(label, t)}
-            </option>
-          ))}
-        </select>
-      )}
-    </SettingItemContainer>
-  );
-};
-
-const SettingsPage: React.FC = () => {
-  const { i18n } = useTranslation();
-  const user = useUser();
-  const navigate = useNavigate();
-
-  const [dialog, setDialog] = useState<
-    | null
-    | "changepassword"
-    | "changeavatar"
-    | "changenickname"
-    | "logout"
-    | "renewregistercode"
-  >(null);
-
-  const [registerCode, setRegisterCode] = useState<undefined | null | string>(
-    undefined,
-  );
-
-  const [bookmarkVisibility, setBookmarkVisibility] =
-    useState<TimelineVisibility>();
-
-  React.useEffect(() => {
-    if (user != null) {
-      void getHttpUserClient()
-        .getBookmarkVisibility(user.username)
-        .then(({ visibility }) => {
-          setBookmarkVisibility(visibility);
-        });
-    } else {
-      setBookmarkVisibility(undefined);
-    }
-  }, [user]);
-
-  React.useEffect(() => {
-    setRegisterCode(undefined);
-  }, [user]);
-
-  React.useEffect(() => {
-    if (user != null && registerCode === undefined) {
-      void getHttpUserClient()
-        .getRegisterCode(user.username)
-        .then((code) => {
-          setRegisterCode(code.registerCode ?? null);
-        });
-    }
-  }, [user, registerCode]);
-
-  const language = i18n.language.slice(0, 2);
-
-  return (
-    <>
-      <div className="container">
-        {user ? (
-          <SettingSection title="settings.subheaders.account">
-            <SettingItemContainer
-              title="settings.myRegisterCode"
-              subtext="settings.myRegisterCodeDesc"
-              onClick={() => setDialog("renewregistercode")}
-            >
-              {registerCode === undefined ? (
-                <Spinner />
-              ) : registerCode === null ? (
-                <span>Noop</span>
-              ) : (
-                <code
-                  className="register-code"
-                  onClick={(event) => {
-                    void navigator.clipboard
-                      .writeText(registerCode)
-                      .then(() => {
-                        pushAlert({
-                          type: "success",
-                          message: "settings.myRegisterCodeCopied",
-                        });
-                      });
-                    event.stopPropagation();
-                  }}
-                >
-                  {registerCode}
-                </code>
-              )}
-            </SettingItemContainer>
-            <ButtonSettingItem
-              title="settings.changeAvatar"
-              onClick={() => setDialog("changeavatar")}
-              first
-            />
-            <ButtonSettingItem
-              title="settings.changeNickname"
-              onClick={() => setDialog("changenickname")}
-            />
-            <SelectSettingsItem
-              title="settings.changeBookmarkVisibility"
-              options={[
-                {
-                  value: "Private",
-                  label: "visibility.private",
-                },
-                {
-                  value: "Register",
-                  label: "visibility.register",
-                },
-                {
-                  value: "Public",
-                  label: "visibility.public",
-                },
-              ]}
-              value={bookmarkVisibility}
-              onSelect={(value) => {
-                void getHttpUserClient()
-                  .putBookmarkVisibility(user.username, {
-                    visibility: value as TimelineVisibility,
-                  })
-                  .then(() => {
-                    setBookmarkVisibility(value as TimelineVisibility);
-                  });
-              }}
-            />
-            <ButtonSettingItem
-              title="settings.changePassword"
-              onClick={() => setDialog("changepassword")}
-              danger
-            />
-            <ButtonSettingItem
-              title="settings.logout"
-              onClick={() => {
-                setDialog("logout");
-              }}
-              danger
-            />
-          </SettingSection>
-        ) : null}
-        <SettingSection title="settings.subheaders.customization">
-          <SelectSettingsItem
-            title="settings.languagePrimary"
-            subtext="settings.languageSecondary"
-            options={[
-              {
-                value: "zh",
-                label: {
-                  type: "custom",
-                  value: "中文",
-                },
-              },
-              {
-                value: "en",
-                label: {
-                  type: "custom",
-                  value: "English",
-                },
-              },
-            ]}
-            value={language}
-            onSelect={(value) => {
-              void i18n.changeLanguage(value);
-            }}
-            first
-          />
-        </SettingSection>
-      </div>
-      <ChangePasswordDialog
-        open={dialog === "changepassword"}
-        close={() => setDialog(null)}
-      />
-      <ConfirmDialog
-        title="settings.dialogConfirmLogout.title"
-        body="settings.dialogConfirmLogout.prompt"
-        onClose={() => setDialog(null)}
-        open={dialog === "logout"}
-        onConfirm={() => {
-          void userService.logout().then(() => {
-            navigate("/");
-          });
-        }}
-      />
-      <ConfirmDialog
-        title="settings.renewRegisterCode"
-        body="settings.renewRegisterCodeDesc"
-        onClose={() => setDialog(null)}
-        open={dialog === "renewregistercode"}
-        onConfirm={() => {
-          if (user == null) throw new UiLogicError();
-          void getHttpUserClient()
-            .renewRegisterCode(user.username)
-            .then(() => {
-              setRegisterCode(undefined);
-            });
-        }}
-      />
-      <ChangeAvatarDialog
-        open={dialog === "changeavatar"}
-        close={() => setDialog(null)}
-      />
-      <ChangeNicknameDialog
-        open={dialog === "changenickname"}
-        close={() => setDialog(null)}
-      />
-    </>
-  );
-};
-
-export default SettingsPage;
-- 
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/about/index.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

