From a9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Fri, 28 Jul 2023 00:40:58 +0800
Subject: ...

---
 FrontEnd/src/views/admin/UserAdmin.tsx | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

(limited to 'FrontEnd/src/views/admin/UserAdmin.tsx')

diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx
index d5179bf5..6003bd5a 100644
--- a/FrontEnd/src/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/views/admin/UserAdmin.tsx
@@ -35,7 +35,7 @@ const CreateUserDialog: React.FC<{
           password,
         })
       }
-      onClose={close}
+      close={close}
       open={open}
       onSuccessAndClose={onSuccess}
     />
@@ -55,7 +55,7 @@ const UserDeleteDialog: React.FC<{
   return (
     <OperationDialog
       open={open}
-      onClose={close}
+      close={close}
       title="admin:user.dialog.delete.title"
       themeColor="danger"
       inputPrompt={() => (
@@ -78,7 +78,7 @@ const UserModifyDialog: React.FC<{
   return (
     <OperationDialog
       open={open}
-      onClose={close}
+      close={close}
       title="admin:user.dialog.modify.title"
       themeColor="danger"
       inputPrompt={() => (
@@ -126,7 +126,7 @@ const UserPermissionModifyDialog: React.FC<{
   return (
     <OperationDialog
       open={open}
-      onClose={close}
+      close={close}
       title="admin:user.dialog.modifyPermissions.title"
       themeColor="danger"
       inputPrompt={() => (
-- 
cgit v1.2.3


From 22e8f24e7f7574915e4c75d3c6a5498f6e621ee8 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 29 Jul 2023 22:00:59 +0800
Subject: ...

---
 FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx  | 15 +++---
 .../src/pages/setting/ChangeNicknameDialog.tsx     |  6 +--
 .../src/pages/setting/ChangePasswordDialog.tsx     |  6 +--
 FrontEnd/src/pages/setting/index.tsx               | 46 +++++++-----------
 FrontEnd/src/views/admin/UserAdmin.tsx             |  8 ++--
 FrontEnd/src/views/center/TimelineCreateDialog.tsx |  2 +-
 FrontEnd/src/views/common/button/LoadingButton.tsx |  5 +-
 .../src/views/common/dialog/OperationDialog.tsx    | 22 +++++----
 FrontEnd/src/views/common/dialog/index.ts          | 56 ++++++++++++++++++++++
 FrontEnd/src/views/common/input/InputGroup.tsx     |  7 ++-
 .../views/timeline/PostPropertyChangeDialog.tsx    |  2 +-
 .../src/views/timeline/TimelineDeleteDialog.tsx    |  2 +-
 .../timeline/TimelinePropertyChangeDialog.tsx      |  2 +-
 13 files changed, 116 insertions(+), 63 deletions(-)
 create mode 100644 FrontEnd/src/views/common/dialog/index.ts

(limited to 'FrontEnd/src/views/admin/UserAdmin.tsx')

diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
index b2a4e2a8..8c8e04fe 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
@@ -9,13 +9,16 @@ import { useUser } from "@/services/user";
 
 import { getHttpUserClient } from "@/http/user";
 
-import ImageCropper, { Clip, applyClipToImage } from "@/views/common/ImageCropper";
+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;
-  close: () => void;
+  onClose: () => void;
 }
 
 const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
@@ -42,12 +45,12 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
   >("select");
 
   const [message, setMessage] = useState<I18nText>(
-    "settings.dialogChangeAvatar.prompt.select"
+    "settings.dialogChangeAvatar.prompt.select",
   );
 
   const trueMessage = convertI18nText(message, t);
 
-  const closeDialog = props.close;
+  const closeDialog = props.onClose;
 
   const close = React.useCallback((): void => {
     if (!(state === "uploading")) {
@@ -92,7 +95,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
         setFile(files[0]);
       }
     },
-    []
+    [],
   );
 
   const onCropNext = React.useCallback(() => {
@@ -140,7 +143,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
         (e: unknown) => {
           setState("error");
           setMessage({ type: "custom", value: (e as AxiosError).message });
-        }
+        },
       );
   }, [user, resultBlob]);
 
diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
index 5606ce94..4d318543 100644
--- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx
@@ -5,11 +5,11 @@ import OperationDialog from "@/views/common/dialog/OperationDialog";
 
 export interface ChangeNicknameDialogProps {
   open: boolean;
-  close: () => void;
+  onClose: () => void;
 }
 
 export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) {
-  const { open, close } = props;
+  const { open, onClose } = props;
 
   const user = useUserLoggedIn();
 
@@ -29,7 +29,7 @@ export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) {
           nickname: newNickname as string,
         });
       }}
-      close={close}
+      onClose={onClose}
     />
   );
 }
diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
index 407f3051..87a970a5 100644
--- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
@@ -9,11 +9,11 @@ import OperationDialog, {
 
 interface ChangePasswordDialogProps {
   open: boolean;
-  close: () => void;
+  onClose: () => void;
 }
 
 export function ChangePasswordDialog(props: ChangePasswordDialogProps) {
-  const { open, close } = props;
+  const { open, onClose } = props;
 
   const navigate = useNavigate();
 
@@ -22,7 +22,7 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) {
   return (
     <OperationDialog
       open={open}
-      close={close}
+      onClose={onClose}
       title="settings.dialogChangePassword.title"
       color="danger"
       inputPrompt="settings.dialogChangePassword.prompt"
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx
index cec81530..12a7670e 100644
--- a/FrontEnd/src/pages/setting/index.tsx
+++ b/FrontEnd/src/pages/setting/index.tsx
@@ -12,6 +12,7 @@ import { useC, Text } from "@/common";
 import { useUser, userService } from "@/services/user";
 import { getHttpUserClient } from "@/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";
@@ -237,18 +238,13 @@ export default function SettingPage() {
   const user = useUser();
   const navigate = useNavigate();
 
-  type DialogName =
-    | "change-password"
-    | "change-avatar"
-    | "change-nickname"
-    | "logout"
-    | "renew-register-code";
-
-  const [dialog, setDialog] = useState<null | DialogName>(null);
-
-  function dialogOpener(name: DialogName): () => void {
-    return () => setDialog(name);
-  }
+  const { dialogPropsMap, createDialogSwitch } = useDialog([
+    "change-password",
+    "change-avatar",
+    "change-nickname",
+    "logout",
+    "renew-register-code",
+  ]);
 
   return (
     <Page noTopPadding>
@@ -257,20 +253,20 @@ export default function SettingPage() {
           <RegisterCodeSettingItem />
           <ButtonSettingItem
             title="settings.changeAvatar"
-            onClick={dialogOpener("change-avatar")}
+            onClick={createDialogSwitch("change-avatar")}
           />
           <ButtonSettingItem
             title="settings.changeNickname"
-            onClick={dialogOpener("change-nickname")}
+            onClick={createDialogSwitch("change-nickname")}
           />
           <ButtonSettingItem
             title="settings.changePassword"
-            onClick={dialogOpener("change-password")}
+            onClick={createDialogSwitch("change-password")}
             danger
           />
           <ButtonSettingItem
             title="settings.logout"
-            onClick={dialogOpener("logout")}
+            onClick={createDialogSwitch("logout")}
             danger
           />
         </SettingSection>
@@ -278,31 +274,21 @@ export default function SettingPage() {
       <SettingSection title="settings.subheader.customization">
         <LanguageChangeSettingItem />
       </SettingSection>
-      <ChangePasswordDialog
-        open={dialog === "change-password"}
-        close={() => setDialog(null)}
-      />
+      <ChangePasswordDialog {...dialogPropsMap["change-password"]} />
       {user && (
         <>
           <ConfirmDialog
             title="settings.dialogConfirmLogout.title"
             body="settings.dialogConfirmLogout.prompt"
-            onClose={() => setDialog(null)}
-            open={dialog === "logout"}
             onConfirm={() => {
               void userService.logout().then(() => {
                 navigate("/");
               });
             }}
+            {...dialogPropsMap["logout"]}
           />
-          <ChangeAvatarDialog
-            open={dialog === "change-avatar"}
-            close={() => setDialog(null)}
-          />
-          <ChangeNicknameDialog
-            open={dialog === "change-nickname"}
-            close={() => setDialog(null)}
-          />
+          <ChangeAvatarDialog {...dialogPropsMap["change-avatar"]} />
+          <ChangeNicknameDialog {...dialogPropsMap["change-nickname"]} />
         </>
       )}
     </Page>
diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx
index 6003bd5a..d5179bf5 100644
--- a/FrontEnd/src/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/views/admin/UserAdmin.tsx
@@ -35,7 +35,7 @@ const CreateUserDialog: React.FC<{
           password,
         })
       }
-      close={close}
+      onClose={close}
       open={open}
       onSuccessAndClose={onSuccess}
     />
@@ -55,7 +55,7 @@ const UserDeleteDialog: React.FC<{
   return (
     <OperationDialog
       open={open}
-      close={close}
+      onClose={close}
       title="admin:user.dialog.delete.title"
       themeColor="danger"
       inputPrompt={() => (
@@ -78,7 +78,7 @@ const UserModifyDialog: React.FC<{
   return (
     <OperationDialog
       open={open}
-      close={close}
+      onClose={close}
       title="admin:user.dialog.modify.title"
       themeColor="danger"
       inputPrompt={() => (
@@ -126,7 +126,7 @@ const UserPermissionModifyDialog: React.FC<{
   return (
     <OperationDialog
       open={open}
-      close={close}
+      onClose={close}
       title="admin:user.dialog.modifyPermissions.title"
       themeColor="danger"
       inputPrompt={() => (
diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx
index 8d4dde10..63742936 100644
--- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx
+++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx
@@ -20,7 +20,7 @@ const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => {
   return (
     <OperationDialog
       open={props.open}
-      close={props.close}
+      onClose={props.close}
       themeColor="success"
       title="home.createDialog.title"
       inputScheme={
diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
index f23369de..bfa5b6b8 100644
--- a/FrontEnd/src/views/common/button/LoadingButton.tsx
+++ b/FrontEnd/src/views/common/button/LoadingButton.tsx
@@ -15,7 +15,8 @@ interface LoadingButtonProps extends React.ComponentPropsWithoutRef<"button"> {
 export default function LoadingButton(props: LoadingButtonProps) {
   const c = useC();
 
-  const { color, text, loading, className, children, ...otherProps } = props;
+  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.");
@@ -23,7 +24,7 @@ export default function LoadingButton(props: LoadingButtonProps) {
 
   return (
     <button
-      disabled={loading}
+      disabled={disabled || loading}
       className={classNames(
         `cru-${color ?? "primary"} cru-button outline cru-loading-button`,
         className,
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
index be3f7158..74b4a5fa 100644
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
@@ -39,7 +39,7 @@ function OperationDialogPrompt(props: OperationDialogPromptProps) {
 
 export interface OperationDialogProps<TData> {
   open: boolean;
-  close: () => void;
+  onClose: () => void;
 
   color?: ThemeColor;
   inputColor?: ThemeColor;
@@ -57,7 +57,7 @@ export interface OperationDialogProps<TData> {
 function OperationDialog<TData>(props: OperationDialogProps<TData>) {
   const {
     open,
-    close,
+    onClose,
     color,
     inputColor,
     title,
@@ -85,13 +85,14 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
 
   const [step, setStep] = useState<Step>({ type: "input" });
 
-  const { inputGroupProps, hasError, setAllDisabled, confirm } = useInputs({
-    init: inputs,
-  });
+  const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } =
+    useInputs({
+      init: inputs,
+    });
 
-  function onClose() {
+  function close() {
     if (step.type !== "process") {
-      close();
+      onClose();
       if (step.type === "success" && onSuccessAndClose) {
         onSuccessAndClose?.(step.data);
       }
@@ -142,12 +143,13 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
             text="operationDialog.cancel"
             color="secondary"
             outline
-            onClick={onClose}
+            onClick={close}
             disabled={isProcessing}
           />
           <LoadingButton
             color={color}
             loading={isProcessing}
+            disabled={hasErrorAndDirty}
             onClick={onConfirm}
           >
             {c("operationDialog.confirm")}
@@ -173,14 +175,14 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
         <OperationDialogPrompt {...promptProps} />
         <hr />
         <div className="cru-dialog-bottom-area">
-          <Button text="operationDialog.ok" color="primary" onClick={onClose} />
+          <Button text="operationDialog.ok" color="primary" onClick={close} />
         </div>
       </div>
     );
   }
 
   return (
-    <Dialog open={open} onClose={onClose}>
+    <Dialog open={open} onClose={close}>
       <div
         className={classNames(
           "cru-operation-dialog-container",
diff --git a/FrontEnd/src/views/common/dialog/index.ts b/FrontEnd/src/views/common/dialog/index.ts
new file mode 100644
index 00000000..e37b9ed2
--- /dev/null
+++ b/FrontEnd/src/views/common/dialog/index.ts
@@ -0,0 +1,56 @@
+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[],
+  initDialog?: D | null,
+): {
+  dialog: D | null;
+  switchDialog: (newDialog: D | null) => void;
+  dialogPropsMap: DialogPropsMap<D>;
+  createDialogSwitch: (newDialog: D | null) => () => void;
+} {
+  const [dialog, setDialog] = useState<D | null>(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),
+        },
+      ]),
+    ) as DialogPropsMap<D>,
+    createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog),
+  };
+}
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx
index 858fa1a5..3d1e3ada 100644
--- a/FrontEnd/src/views/common/input/InputGroup.tsx
+++ b/FrontEnd/src/views/common/input/InputGroup.tsx
@@ -141,6 +141,7 @@ export type ConfirmResult =
 export function useInputs(options: { init: Initializer }): {
   inputGroupProps: InputGroupProps;
   hasError: boolean;
+  hasErrorAndDirty: boolean;
   confirm: () => ConfirmResult;
   setAllDisabled: (disabled: boolean) => void;
 } {
@@ -260,6 +261,9 @@ export function useInputs(options: { init: Initializer }): {
     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,
@@ -280,7 +284,8 @@ export function useInputs(options: { init: Initializer }): {
         });
       },
     },
-    hasError: Object.keys(data.errors).length > 0,
+    hasError,
+    hasErrorAndDirty: hasError && hasDirty,
     confirm() {
       const newDirties = createAllDirties();
       const newErrors = validator?.(data.values, scheme.inputs) ?? {};
diff --git a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx
index 76f542c1..fc55185c 100644
--- a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx
+++ b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx
@@ -15,7 +15,7 @@ function PostPropertyChangeDialog(props: {
   return (
     <OperationDialog
       title="timeline.changePostPropertyDialog.title"
-      close={onClose}
+      onClose={onClose}
       open={open}
       inputScheme={[
         {
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
index 3b5ba42f..c960b3c2 100644
--- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
+++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
@@ -20,7 +20,7 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
   return (
     <OperationDialog
       open={props.open}
-      close={props.close}
+      onClose={props.close}
       title="timeline.deleteDialog.title"
       themeColor="danger"
       inputPrompt={() => {
diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
index a0eebdbb..bd5bef4c 100644
--- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
+++ b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
@@ -55,7 +55,7 @@ const TimelinePropertyChangeDialog: React.FC<
         ] as const
       }
       open={props.open}
-      close={props.close}
+      onClose={props.close}
       onProcess={([newTitle, newVisibility, newDescription, newColor]) => {
         const req: HttpTimelinePatchRequest = {};
         if (newTitle !== timeline.title) {
-- 
cgit v1.2.3


From 645a88e7e35d15cec6106709c42b071bec045e0d Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Wed, 2 Aug 2023 02:52:07 +0800
Subject: ...

---
 BackEnd/Timeline.Tests/Timeline.Tests.csproj       |  20 +-
 BackEnd/Timeline.Tests/packages.lock.json          | 307 ++++++++--------
 BackEnd/Timeline/Timeline.csproj                   |  18 +-
 BackEnd/Timeline/packages.lock.json                | 171 ++++-----
 FrontEnd/.eslintignore                             |   1 +
 FrontEnd/src/App.tsx                               |   4 -
 FrontEnd/src/migrating/admin/Admin.tsx             |  27 ++
 FrontEnd/src/migrating/admin/AdminNav.tsx          |  29 ++
 FrontEnd/src/migrating/admin/MoreAdmin.tsx         |   7 +
 FrontEnd/src/migrating/admin/UserAdmin.tsx         | 301 ++++++++++++++++
 FrontEnd/src/migrating/admin/index.css             |  33 ++
 FrontEnd/src/migrating/admin/index.tsx             |   7 +
 FrontEnd/src/migrating/center/CenterBoards.tsx     | 131 +++++++
 FrontEnd/src/migrating/center/TimelineBoard.tsx    | 390 +++++++++++++++++++++
 .../src/migrating/center/TimelineCreateDialog.tsx  |  57 +++
 FrontEnd/src/migrating/center/index.css            |  43 +++
 FrontEnd/src/migrating/center/index.tsx            |  60 ++++
 FrontEnd/src/services/alert.ts                     |   4 +-
 FrontEnd/src/views/admin/Admin.tsx                 |  27 --
 FrontEnd/src/views/admin/AdminNav.tsx              |  29 --
 FrontEnd/src/views/admin/MoreAdmin.tsx             |   7 -
 FrontEnd/src/views/admin/UserAdmin.tsx             | 304 ----------------
 FrontEnd/src/views/admin/index.css                 |  33 --
 FrontEnd/src/views/admin/index.tsx                 |   7 -
 FrontEnd/src/views/center/CenterBoards.tsx         | 131 -------
 FrontEnd/src/views/center/TimelineBoard.tsx        | 390 ---------------------
 FrontEnd/src/views/center/TimelineCreateDialog.tsx |  57 ---
 FrontEnd/src/views/center/index.css                |  43 ---
 FrontEnd/src/views/center/index.tsx                |  60 ----
 FrontEnd/src/views/common/button/LoadingButton.css |   2 +-
 FrontEnd/src/views/search/index.css                |  15 -
 FrontEnd/src/views/search/index.tsx                | 131 -------
 FrontEnd/tsconfig.json                             |   3 +
 33 files changed, 1336 insertions(+), 1513 deletions(-)
 create mode 100644 FrontEnd/src/migrating/admin/Admin.tsx
 create mode 100644 FrontEnd/src/migrating/admin/AdminNav.tsx
 create mode 100644 FrontEnd/src/migrating/admin/MoreAdmin.tsx
 create mode 100644 FrontEnd/src/migrating/admin/UserAdmin.tsx
 create mode 100644 FrontEnd/src/migrating/admin/index.css
 create mode 100644 FrontEnd/src/migrating/admin/index.tsx
 create mode 100644 FrontEnd/src/migrating/center/CenterBoards.tsx
 create mode 100644 FrontEnd/src/migrating/center/TimelineBoard.tsx
 create mode 100644 FrontEnd/src/migrating/center/TimelineCreateDialog.tsx
 create mode 100644 FrontEnd/src/migrating/center/index.css
 create mode 100644 FrontEnd/src/migrating/center/index.tsx
 delete mode 100644 FrontEnd/src/views/admin/Admin.tsx
 delete mode 100644 FrontEnd/src/views/admin/AdminNav.tsx
 delete mode 100644 FrontEnd/src/views/admin/MoreAdmin.tsx
 delete mode 100644 FrontEnd/src/views/admin/UserAdmin.tsx
 delete mode 100644 FrontEnd/src/views/admin/index.css
 delete mode 100644 FrontEnd/src/views/admin/index.tsx
 delete mode 100644 FrontEnd/src/views/center/CenterBoards.tsx
 delete mode 100644 FrontEnd/src/views/center/TimelineBoard.tsx
 delete mode 100644 FrontEnd/src/views/center/TimelineCreateDialog.tsx
 delete mode 100644 FrontEnd/src/views/center/index.css
 delete mode 100644 FrontEnd/src/views/center/index.tsx
 delete mode 100644 FrontEnd/src/views/search/index.css
 delete mode 100644 FrontEnd/src/views/search/index.tsx

(limited to 'FrontEnd/src/views/admin/UserAdmin.tsx')

diff --git a/BackEnd/Timeline.Tests/Timeline.Tests.csproj b/BackEnd/Timeline.Tests/Timeline.Tests.csproj
index 57694c2b..3983bc0b 100644
--- a/BackEnd/Timeline.Tests/Timeline.Tests.csproj
+++ b/BackEnd/Timeline.Tests/Timeline.Tests.csproj
@@ -8,15 +8,21 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="coverlet.collector" Version="3.2.0" />
-        <PackageReference Include="FluentAssertions" Version="6.10.0" />
+        <PackageReference Include="coverlet.collector" Version="6.0.0">
+          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+          <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
+        <PackageReference Include="FluentAssertions" Version="6.11.0" />
         <PackageReference Include="MartinCostello.Logging.XUnit" Version="0.3.0" />
-        <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.3" />
-        <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" />
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
+        <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.9" />
+        <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.9" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
         <PackageReference Include="Moq" Version="4.18.4" />
-        <PackageReference Include="xunit" Version="2.4.2" />
-        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
+        <PackageReference Include="xunit" Version="2.5.0" />
+        <PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
+          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+          <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
     </ItemGroup>
 
     <ItemGroup>
diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json
index cc7108a4..cb747b24 100644
--- a/BackEnd/Timeline.Tests/packages.lock.json
+++ b/BackEnd/Timeline.Tests/packages.lock.json
@@ -4,15 +4,15 @@
     "net7.0": {
       "coverlet.collector": {
         "type": "Direct",
-        "requested": "[3.2.0, )",
-        "resolved": "3.2.0",
-        "contentHash": "xjY8xBigSeWIYs4I7DgUHqSNoGqnHi7Fv7/7RZD02rvZyG3hlsjnQKiVKVWKgr9kRKgmV+dEfu8KScvysiC0Wg=="
+        "requested": "[6.0.0, )",
+        "resolved": "6.0.0",
+        "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ=="
       },
       "FluentAssertions": {
         "type": "Direct",
-        "requested": "[6.10.0, )",
-        "resolved": "6.10.0",
-        "contentHash": "Da3YsiRDnOHKBfxutjnupL1rOX0K/jnG6crn5AgwukeqZ/yi+HNCOFshic01ke0ztZFWzpfQMXH8fO9aAbG0Gw==",
+        "requested": "[6.11.0, )",
+        "resolved": "6.11.0",
+        "contentHash": "aBaagwdNtVKkug1F3imGXUlmoBd8ZUZX8oQ5niThaJhF79SpESe1Gzq7OFuZkQdKD5Pa4Mone+jrbas873AT4g==",
         "dependencies": {
           "System.Configuration.ConfigurationManager": "4.4.0"
         }
@@ -30,31 +30,31 @@
       },
       "Microsoft.AspNetCore.SignalR.Client": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "kyxW20UJwTf1VZuiK8jWhz9tx1NhfzCG5KoOlHDnpr0dy2utlHil9xgdlsAC5V46K1Dser3DZDbCn0Qq8x4K1w==",
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "tXSCmD/oZimbnzOfVSz8CGrKy/OLDvWJ87Hp1Yg8IkG/8ek3532Hk8Ujf/gNjZy1jRLT2s4jcQS7iwGguhvi3g==",
         "dependencies": {
-          "Microsoft.AspNetCore.Http.Connections.Client": "7.0.3",
-          "Microsoft.AspNetCore.SignalR.Client.Core": "7.0.3"
+          "Microsoft.AspNetCore.Http.Connections.Client": "7.0.9",
+          "Microsoft.AspNetCore.SignalR.Client.Core": "7.0.9"
         }
       },
       "Microsoft.AspNetCore.TestHost": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "wS+gBqK8O6ASpGjEoRdxKR4qb/Hqt+RumM1bMfKxkjYpsBl4UMh8ClmXzU42omn3sWCpPqxZIBSIO3F4zEYPzQ==",
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "QHbCXxCpVEKFyZ80JspVjIkjy6LsavJXGUc1qHN3bJk4SipVMTM8rq/lhYM8Gu1xJJX8pH/AqguFa391Nx9RTw==",
         "dependencies": {
           "System.IO.Pipelines": "7.0.0"
         }
       },
       "Microsoft.NET.Test.Sdk": {
         "type": "Direct",
-        "requested": "[17.5.0, )",
-        "resolved": "17.5.0",
-        "contentHash": "IJ4eSPcsRbwbAZehh1M9KgejSy0u3d0wAdkJytfCh67zOaCl5U3ltruUEe15MqirdRqGmm/ngbjeaVeGapSZxg==",
+        "requested": "[17.6.3, )",
+        "resolved": "17.6.3",
+        "contentHash": "MglaNTl646dC2xpHKotSk1xscmHO5uV3x3NK057IUA9BM3Wgl16WMEb9ptGczk518JfLd1+Th5OAYwnoWgHQQQ==",
         "dependencies": {
-          "Microsoft.CodeCoverage": "17.5.0",
-          "Microsoft.TestPlatform.TestHost": "17.5.0"
+          "Microsoft.CodeCoverage": "17.6.3",
+          "Microsoft.TestPlatform.TestHost": "17.6.3"
         }
       },
       "Moq": {
@@ -68,20 +68,20 @@
       },
       "xunit": {
         "type": "Direct",
-        "requested": "[2.4.2, )",
-        "resolved": "2.4.2",
-        "contentHash": "6Mj73Ont3zj2CJuoykVJfE0ZmRwn7C+pTuRP8c4bnaaTFjwNG6tGe0prJ1yIbMe9AHrpDys63ctWacSsFJWK/w==",
+        "requested": "[2.5.0, )",
+        "resolved": "2.5.0",
+        "contentHash": "f2V5wuAdoaq0mRTt9UBmPbVex9HcwFYn+y7WaKUz5Xpakcrv7lhtQWBJUWNY4N3Z+o+atDBLyAALM1QWx04C6Q==",
         "dependencies": {
-          "xunit.analyzers": "1.0.0",
-          "xunit.assert": "2.4.2",
-          "xunit.core": "[2.4.2]"
+          "xunit.analyzers": "1.2.0",
+          "xunit.assert": "2.5.0",
+          "xunit.core": "[2.5.0]"
         }
       },
       "xunit.runner.visualstudio": {
         "type": "Direct",
-        "requested": "[2.4.5, )",
-        "resolved": "2.4.5",
-        "contentHash": "OwHamvBdUKgqsXfBzWiCW/O98BTx81UKzx2bieIOQI7CZFE5NEQZGi8PBQGIKawDW96xeRffiNf20SjfC0x9hw=="
+        "requested": "[2.5.0, )",
+        "resolved": "2.5.0",
+        "contentHash": "+Gp9vuC2431yPyKB15YrOTxCuEAErBQUTIs6CquumX1F073UaPHGW0VE/XVJLMh9W4sXdz3TBkcHdFWZrRn2Hw=="
       },
       "AutoMapper": {
         "type": "Transitive",
@@ -93,10 +93,10 @@
       },
       "AutoMapper.Extensions.Microsoft.DependencyInjection": {
         "type": "Transitive",
-        "resolved": "12.0.0",
-        "contentHash": "XCJ4E3oKrbRl1qY9Mr+7uyC0xZj1+bqQjmQRWTiTKiVuuXTny+7YFWHi20tPjwkMukLbicN6yGlDy5PZ4wyi1w==",
+        "resolved": "12.0.1",
+        "contentHash": "+g/K+Vpe3gGMKGzjslMOdqNlkikScDjWfVvmWTayrDHaG/n2pPmFBMa+jKX1r/h6BDGFdkyRjAuhFE3ykW+r1g==",
         "dependencies": {
-          "AutoMapper": "12.0.0",
+          "AutoMapper": "[12.0.1]",
           "Microsoft.Extensions.Options": "6.0.0"
         }
       },
@@ -110,43 +110,43 @@
       },
       "Markdig": {
         "type": "Transitive",
-        "resolved": "0.30.4",
-        "contentHash": "9LL7Mzubwf6QmOTZIqf+vERiinqPA1H+irBA38POhr93W/it00AvFWaCtF80+8VJTooFmcAJyZT2f60t1f88eQ=="
+        "resolved": "0.31.0",
+        "contentHash": "CEdZ3KyFfz5IPMrofhCOSrc4O2KNvSXSMXt3fv0W5AFCsk6a/JhKZuTBxGSadNt8n4zyyd/xXfoUMvr4ZbZ4Fw=="
       },
       "Microsoft.AspNetCore.Connections.Abstractions": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "uR20YarXQQfazLUtfn2KBNRgROtsvsNRRku9+s0o1MpkVn6aWhJ0udzMdTUZ3hrXo/VP3UoRbi9f4OYlJF0DLA==",
+        "resolved": "7.0.9",
+        "contentHash": "T/A7MkuL0g4r/oNuASR2gIukWoG2360/tuS9RoiU1dOwTlMYthHJaK8NEpswMihcImCDryiHp5dJCPTZHIs9TQ==",
         "dependencies": {
-          "Microsoft.Extensions.Features": "7.0.3",
+          "Microsoft.Extensions.Features": "7.0.9",
           "System.IO.Pipelines": "7.0.0"
         }
       },
       "Microsoft.AspNetCore.Http.Connections.Client": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "t2+AReXjZeL47pq50gg/gfmX3u0jK++LL8pg0IoyG9IjhlFKl/okZcvXWp/mMPKRyMmpKJvVb++tQLMyIOEYlw==",
+        "resolved": "7.0.9",
+        "contentHash": "41NrQVGv+M5KpujoaUwPs8DGaQrkvhJvaotuvdyvsuTPlcpzqBy+bmQs2eUXEpOfFxOWL9uqZcwLpoOkCOn3/A==",
         "dependencies": {
-          "Microsoft.AspNetCore.Http.Connections.Common": "7.0.3",
-          "Microsoft.Extensions.Logging.Abstractions": "7.0.0",
+          "Microsoft.AspNetCore.Http.Connections.Common": "7.0.9",
+          "Microsoft.Extensions.Logging.Abstractions": "7.0.1",
           "Microsoft.Extensions.Options": "7.0.1"
         }
       },
       "Microsoft.AspNetCore.Http.Connections.Common": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "egbJZ83GxjFt6WWD9Oab6lLf4eRHyxA0mRzBX/2GIX4nXoLt0vvIk9LciCRhwWHjFkVj8zOBfjEQ58RXfKOs4Q==",
+        "resolved": "7.0.9",
+        "contentHash": "GnhaII0AWIAuUTJ3TT02yUOAPlVP4p7zxi40ysQYSLUiD6dEODJc9d3ugFgM8EMnrgYiKw6fbLyn4dixls8CCg==",
         "dependencies": {
-          "Microsoft.AspNetCore.Connections.Abstractions": "7.0.3"
+          "Microsoft.AspNetCore.Connections.Abstractions": "7.0.9"
         }
       },
       "Microsoft.AspNetCore.SignalR.Client.Core": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "QOwhLtPcuHEqT/pkxF6qRnclaG28Pq4nlvimlEMpEAuiq/+PIM2TX/pGuZ3CO+yrWwuSlqA7Jg6AWA3hoUleuw==",
+        "resolved": "7.0.9",
+        "contentHash": "+rWA87MIbhUQi2fKJUTc9cRJgvHfZJdJ32gnIUxCHZtvwAHtfLG7Lc7KnJXiur6yLtG0CqLOANSfBXr/OOUqQw==",
         "dependencies": {
-          "Microsoft.AspNetCore.SignalR.Common": "7.0.3",
-          "Microsoft.AspNetCore.SignalR.Protocols.Json": "7.0.3",
+          "Microsoft.AspNetCore.SignalR.Common": "7.0.9",
+          "Microsoft.AspNetCore.SignalR.Protocols.Json": "7.0.9",
           "Microsoft.Extensions.DependencyInjection": "7.0.0",
           "Microsoft.Extensions.Logging": "7.0.0",
           "System.Threading.Channels": "7.0.0"
@@ -154,33 +154,33 @@
       },
       "Microsoft.AspNetCore.SignalR.Common": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "Pm4+wwzr7ULOQrq6tlLuLMdtY2YYtXkHz90oMM+CSpV1SKlvfLabRSevd4No2U7PVykAan7kYgKN8HSxTYf1RA==",
+        "resolved": "7.0.9",
+        "contentHash": "u+mm77kNAm2qDjyXhZJnNDY1g0W6Tql4TbLClITMIK54KmOn+lzTsdpFBb41qJ1Jr1Qj+nC0yAXffEga008T6g==",
         "dependencies": {
-          "Microsoft.AspNetCore.Connections.Abstractions": "7.0.3",
+          "Microsoft.AspNetCore.Connections.Abstractions": "7.0.9",
           "Microsoft.Extensions.Options": "7.0.1"
         }
       },
       "Microsoft.AspNetCore.SignalR.Protocols.Json": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "+DUVF6FdhMC4v7g/QaNor7hVBCy7+FHjC6h2HByvHxqJSj7i1y/m/4hg8nOTMhrj9BDbPeGK0b9NRrtrM83QmQ==",
+        "resolved": "7.0.9",
+        "contentHash": "0kUF1Vqd+Z42ihHjSShSQfayheIZENn1WamPMd/PB9+upEYP8jJDr+8MtCeYNwzxhmzu2UqfgeTeWJ96SZpzhQ==",
         "dependencies": {
-          "Microsoft.AspNetCore.SignalR.Common": "7.0.3"
+          "Microsoft.AspNetCore.SignalR.Common": "7.0.9"
         }
       },
       "Microsoft.AspNetCore.SpaServices.Extensions": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "dh2ZmM+4Y3b3pCBndGuvpBIp1OOOK+CeZruphDKoFWgmxfN/RYmV0QvOS3u1McVb1yFmcVcXgVx0DEW5+WRkFg==",
+        "resolved": "7.0.9",
+        "contentHash": "qPuCTxQD832rs6vH5W8nVowU+BiWh45uH8+WnhEFiqgjGAGO8gw9Yf2aO4m0+VBi60kLwM8KYwYyLlpm0A8tbA==",
         "dependencies": {
           "Microsoft.Extensions.FileProviders.Physical": "7.0.0"
         }
       },
       "Microsoft.CodeCoverage": {
         "type": "Transitive",
-        "resolved": "17.5.0",
-        "contentHash": "6FQo0O6LKDqbCiIgVQhJAf810HSjFlOj7FunWaeOGDKxy8DAbpHzPk4SfBTXz9ytaaceuIIeR6hZgplt09m+ig=="
+        "resolved": "17.6.3",
+        "contentHash": "Gorg6F1dOxlI28yHYKhbQ3pOOfHeW6sUfsmwFQFaIV+xttUAZ+l8KarHIfsR+rBAnjY9VH71BXvPXBuObCkXsw=="
       },
       "Microsoft.CSharp": {
         "type": "Transitive",
@@ -189,19 +189,19 @@
       },
       "Microsoft.Data.Sqlite.Core": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "pCmzLLWTIrIv94o7JtQ1qcPD0oc1YNY9XvlO6/tOF9YCcUfDZ3Tx9Z//CM7hFnprduHFPekif7jteBc/sXQ31Q==",
+        "resolved": "7.0.9",
+        "contentHash": "ow2PPoeW0yFc7NhexacQUw/LVjkO1mLK3VZAxhVIVjmQWlgYl/4mo9/U7uz+z75I+ZN6LUvq9M0ftU3IE75Ilg==",
         "dependencies": {
           "SQLitePCLRaw.core": "2.1.4"
         }
       },
       "Microsoft.EntityFrameworkCore": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "6XUI2YGoaLMoP9KGaqWmmd4B2n5bpQbXrVRpH20Et3YjQ0Rn3Ia6HM/ANcSq9rBfjfUySgo9SwUZgQ4m4PF3LQ==",
+        "resolved": "7.0.9",
+        "contentHash": "9YuCdQWuRAmYYHqwW1h5ukKMC1fmNvcVHdp3gb8zdHxwSQz7hkGpYOBEjm6dRXRmGRkpUyHL8rwUz4kd53Ev0A==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore.Abstractions": "7.0.3",
-          "Microsoft.EntityFrameworkCore.Analyzers": "7.0.3",
+          "Microsoft.EntityFrameworkCore.Abstractions": "7.0.9",
+          "Microsoft.EntityFrameworkCore.Analyzers": "7.0.9",
           "Microsoft.Extensions.Caching.Memory": "7.0.0",
           "Microsoft.Extensions.DependencyInjection": "7.0.0",
           "Microsoft.Extensions.Logging": "7.0.0"
@@ -209,39 +209,39 @@
       },
       "Microsoft.EntityFrameworkCore.Abstractions": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "0NaFBZykUlQHknddRuKY4v+MFZX/AW+v2V85dgj7abIlt+kL3GWa7QNH5S1084VLf1u+dq1SnhZsOvykc3Y0sA=="
+        "resolved": "7.0.9",
+        "contentHash": "cfY6Fn7cnP/5pXndL8QYaey/B2nGn1fwze5aSaMJymmbqwqYzFiszHuWbsdVBCDYJc8ok7eB1m/nCc3/rltulQ=="
       },
       "Microsoft.EntityFrameworkCore.Analyzers": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "CLyRWFLwaOUZNPEia/aBMzFxZqm/ITKt3B+yUFtrg4Ys5VF3n2gvneuItC9IhpeOcjfdSgu/yUKf8y/IsNHs5A=="
+        "resolved": "7.0.9",
+        "contentHash": "VvqFD3DdML6LhPCAR/lG2xNX66juylC8v57yUAAYnUSdEUOMRi3lNoT4OrNdG0rere3UOQPhvVl5FH2QdyoF8Q=="
       },
       "Microsoft.EntityFrameworkCore.Relational": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "RxNNjtTrxsMtdBtgoXGRSy8uCXaBHaVzIonTeo7+Ys+N0yEWwhf2E74cxneyunMi13Ezlld10ecCHlDubEU/Pw==",
+        "resolved": "7.0.9",
+        "contentHash": "u7iN6cNd6SJUlpdk24JVIbkji/UbkEEQ7pXncTyT4eXXj+Hz2y4NSZFOAywPGcioIgX1YzbKWDiJhk7hjSFxBQ==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore": "7.0.3",
+          "Microsoft.EntityFrameworkCore": "7.0.9",
           "Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
         }
       },
       "Microsoft.EntityFrameworkCore.Sqlite": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "bNWgYJsT6+0I98RSIY0rZAYu6NkLFgPvWPO1LKTsBqG6iwhCW3M38JGDVOOSf69N9HUZz9ilKNLTyYKNRQkyGw==",
+        "resolved": "7.0.9",
+        "contentHash": "otP72XqfG9UdO0+3O2p1UEx4MLXSAulXc8tIvuq+v1KPtBj/GuMZeJIz+eCu09Xv6fJ6k7aUb8J/x+H0rqOjjA==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore.Sqlite.Core": "7.0.3",
+          "Microsoft.EntityFrameworkCore.Sqlite.Core": "7.0.9",
           "SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
         }
       },
       "Microsoft.EntityFrameworkCore.Sqlite.Core": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "w0yzGr1IDY4sp+RkFnVlWVF1N2lmaJHCmzVldZ/e36bXdT95E8N7/+JYWwKm1nLWEpS6naaoNtqQO6buRHTfAg==",
+        "resolved": "7.0.9",
+        "contentHash": "uLy1swPL4AD6BQsu3SEIzSWKc6gIlmoSAYZX39VnbrqvzkGmMsgEgdCsJIZvdjAW/vzhPazACKMYna1iif04sg==",
         "dependencies": {
-          "Microsoft.Data.Sqlite.Core": "7.0.3",
-          "Microsoft.EntityFrameworkCore.Relational": "7.0.3",
+          "Microsoft.Data.Sqlite.Core": "7.0.9",
+          "Microsoft.EntityFrameworkCore.Relational": "7.0.9",
           "Microsoft.Extensions.DependencyModel": "7.0.0"
         }
       },
@@ -302,8 +302,8 @@
       },
       "Microsoft.Extensions.Features": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "LnHGYYYi4dX5XcW1J4QPyMqfWw6cKF9Hp23f3BMTl3kytUGv19K6E9nWA83/SVahzVLwL+RrR02o14VUMBUlug=="
+        "resolved": "7.0.9",
+        "contentHash": "IJ2vdDt2OYCKyJ7ZJPIZKa4b0M0tsG36h0QUt1d/E8IMAnjIncI+1i9Am0nmheD/wpcVd9eDykiV4dklcwUd3Q=="
       },
       "Microsoft.Extensions.FileProviders.Abstractions": {
         "type": "Transitive",
@@ -349,8 +349,8 @@
       },
       "Microsoft.Extensions.Logging.Abstractions": {
         "type": "Transitive",
-        "resolved": "7.0.0",
-        "contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw=="
+        "resolved": "7.0.1",
+        "contentHash": "pkeBFx0vqMW/A3aUVHh7MPu3WkBhaVlezhSZeb1c9XD0vUReYH1TLFSy5MxJgZfmz5LZzYoErMorlYZiwpOoNA=="
       },
       "Microsoft.Extensions.Options": {
         "type": "Transitive",
@@ -368,8 +368,8 @@
       },
       "Microsoft.NETCore.Platforms": {
         "type": "Transitive",
-        "resolved": "5.0.0",
-        "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
+        "resolved": "1.1.0",
+        "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
       },
       "Microsoft.NETCore.Targets": {
         "type": "Transitive",
@@ -378,19 +378,19 @@
       },
       "Microsoft.TestPlatform.ObjectModel": {
         "type": "Transitive",
-        "resolved": "17.5.0",
-        "contentHash": "QwiBJcC/oEA1kojOaB0uPWOIo4i6BYuTBBYJVhUvmXkyYqZ2Ut/VZfgi+enf8LF8J4sjO98oRRFt39MiRorcIw==",
+        "resolved": "17.6.3",
+        "contentHash": "gSqtX3RvcFisaLPs6sKXdZkSwUix83NQ9nOU/w6pYrHTl+d8GsVHSL9rvDNxMgoV5BNOdyU7zK7JOfbSaVMDWQ==",
         "dependencies": {
-          "NuGet.Frameworks": "5.11.0",
+          "NuGet.Frameworks": "6.5.0",
           "System.Reflection.Metadata": "1.6.0"
         }
       },
       "Microsoft.TestPlatform.TestHost": {
         "type": "Transitive",
-        "resolved": "17.5.0",
-        "contentHash": "X86aikwp9d4SDcBChwzQYZihTPGEtMdDk+9t64emAl7N0Tq+OmlLAoW+Rs+2FB2k6QdUicSlT4QLO2xABRokaw==",
+        "resolved": "17.6.3",
+        "contentHash": "lrgRXKFfIZSPlhuoQGLtciO/osL+4oADYEYb0d5or7n7YyJATIWespq3lRgz2IQpRh6N7cm0DnCOWeZiCRGzxA==",
         "dependencies": {
-          "Microsoft.TestPlatform.ObjectModel": "17.5.0",
+          "Microsoft.TestPlatform.ObjectModel": "17.6.3",
           "Newtonsoft.Json": "13.0.1"
         }
       },
@@ -406,8 +406,8 @@
       },
       "Namotion.Reflection": {
         "type": "Transitive",
-        "resolved": "2.1.0",
-        "contentHash": "9t63RauDp+CWzMCcCRAGXLRqEVIw0djYisGaDWhgHuXSaz/Djjpp9gpumCWVLpuDHLNf4HUmYWJeBt4AUyJSWA==",
+        "resolved": "2.1.2",
+        "contentHash": "7tSHAzX8GWKy0qrW6OgQWD7kAZiqzhq+m1503qczuwuK6ZYhOGCQUxw+F3F4KkRM70aB6RMslsRVSCFeouIehw==",
         "dependencies": {
           "Microsoft.CSharp": "4.3.0"
         }
@@ -470,85 +470,85 @@
       },
       "NJsonSchema": {
         "type": "Transitive",
-        "resolved": "10.8.0",
-        "contentHash": "lChjsLWaxyvElh4WJjVhdIiCtx7rimYGFTxtSi2pAkZf0ZnKaXYIX484HCVyzbDDHejDZPgOrcfAJ3kqNSTONw==",
+        "resolved": "10.9.0",
+        "contentHash": "IBPo6Srxn2MEcIFM3HdM4QImrJbsIeujENQyzHL2Pv6wLsKSYAyAEilecRqaLOhoy3snEiPLx7hhv7opbhOxKQ==",
         "dependencies": {
-          "Namotion.Reflection": "2.1.0",
+          "Namotion.Reflection": "2.1.2",
           "Newtonsoft.Json": "9.0.1"
         }
       },
       "NJsonSchema.Yaml": {
         "type": "Transitive",
-        "resolved": "10.8.0",
-        "contentHash": "sygi7CN1AQHFNfpk/RRgsAMl+5tTM65Rajau3QPKoflJoZbxPQLBgZZrH7SMINumkW61JqDy6EhKfHsxngTzqw==",
+        "resolved": "10.9.0",
+        "contentHash": "lGOtsydEnX1mVLT2Cyre7mST7K7crveYtai9FGC2i7COKEMEFjsfbnUtgXSHKxS/bzxVEIjLa49tR6xw34WkEw==",
         "dependencies": {
           "Microsoft.CSharp": "4.4.1",
-          "NJsonSchema": "10.8.0",
+          "NJsonSchema": "10.9.0",
           "YamlDotNet": "11.2.1"
         }
       },
       "NSwag.Annotations": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "2zDLZXEU1PVuvdMc1k+SIkLonp8vPRHAkto6qQfCeCPqhDaF82+FOWe3TLJIYBS8IC7aV/mT+eoqJLeZvXiN4A=="
+        "resolved": "13.19.0",
+        "contentHash": "ukpuIdZTczGUwyIAz954xiefikAUShtEoColOODeKilKGcB5CXn1Q6j3D/Er75z2+qMA3qieXb386xAyW2/v+Q=="
       },
       "NSwag.AspNetCore": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "ueewpltbJpZGWgR3BkLXQRX1g7ZtzNl7sT5DI90lNufJhLWnjC5nAfxRTP4J8nqO1vR79zLYzzA5ZwsusZaTpA==",
+        "resolved": "13.19.0",
+        "contentHash": "7ZPAHrJCMPuDN3kE5bZIckWIVtorMj3YCU0bYu83CqVBsOGlxUy6VS8CnmLHlmAJ5gxEA+RD+KGepJvk3u+ygQ==",
         "dependencies": {
           "Microsoft.Extensions.ApiDescription.Server": "6.0.3",
           "Microsoft.Extensions.FileProviders.Embedded": "[7.0.0, 8.0.0)",
-          "NSwag.Annotations": "13.18.2",
-          "NSwag.Core": "13.18.2",
-          "NSwag.Core.Yaml": "13.18.2",
-          "NSwag.Generation": "13.18.2",
-          "NSwag.Generation.AspNetCore": "13.18.2"
+          "NSwag.Annotations": "13.19.0",
+          "NSwag.Core": "13.19.0",
+          "NSwag.Core.Yaml": "13.19.0",
+          "NSwag.Generation": "13.19.0",
+          "NSwag.Generation.AspNetCore": "13.19.0"
         }
       },
       "NSwag.Core": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "QM6OnVtUkD62fi0lgFF65KbZ0Y8ToGD+Rb4W3Nz6ymCWWfVbMwkn8xUxIjWSLsOEYJCbLvBBeGt+oBNrJwxb+A==",
+        "resolved": "13.19.0",
+        "contentHash": "UkPCsEJ5WaE6mcEjarZZfMPyyRPHOWK9YuFvxNjrSHclCxVUZ7jgFtNGMX0wy0WvXkmA5b6H9IoaAJECMxcu/g==",
         "dependencies": {
-          "NJsonSchema": "10.8.0",
+          "NJsonSchema": "10.9.0",
           "Newtonsoft.Json": "10.0.1"
         }
       },
       "NSwag.Core.Yaml": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "y+h+4jrN3BC0B3v1Qg0Fr3jrGTB5nR5RAn5ZG5qBveiazeB9VDfR5cP9URN6wb3VypahHRzYryUB2KFZuiS22A==",
+        "resolved": "13.19.0",
+        "contentHash": "F97HsEclpnydInq1Wrd74WDQePHQ7S+e4yKi4BiYi/hLy6s88YlpRFB3R5Bx64iRkGKesU/uGiovKtFevpW2Bw==",
         "dependencies": {
-          "NJsonSchema.Yaml": "10.8.0",
-          "NSwag.Core": "13.18.2",
+          "NJsonSchema.Yaml": "10.9.0",
+          "NSwag.Core": "13.19.0",
           "YamlDotNet": "11.2.1"
         }
       },
       "NSwag.Generation": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "hmdi+kXwKTgxu2aYgaILDw2nKvco1/wEd1RXaVNIz4ck6yHTJ58ANVcUjN3ytmL//f+hKEKWZznTi4YQW7jtpQ==",
+        "resolved": "13.19.0",
+        "contentHash": "TdBY+P6WGElaHIAzYaT9te141y46UCUTjlGgGKi5eR4ZXHE/JhmrXXU8kr3zFHWw7gk6TeOKy5LnPlJaxNXb5Q==",
         "dependencies": {
-          "NJsonSchema": "10.8.0",
-          "NSwag.Core": "13.18.2",
+          "NJsonSchema": "10.9.0",
+          "NSwag.Core": "13.19.0",
           "Newtonsoft.Json": "10.0.1"
         }
       },
       "NSwag.Generation.AspNetCore": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "D+yfRbXtv/wDan1oEkskGWejlCiUTdLlV93qynay1Xp1HmnC0XYspjLkfMGr9kqYX4CY9SrMUnDUsRNeyOoR/g==",
+        "resolved": "13.19.0",
+        "contentHash": "1ne3rqJtWTUbZuN/kyCTk1cT36ZLXZs2/tE6Kvhb8MA6GySbwSwaLIJ1eHFn6dksp5+Ic5EhI1ZL6wrqapQgsg==",
         "dependencies": {
           "Microsoft.Extensions.DependencyInjection.Abstractions": "[7.0.0, 8.0.0)",
           "Microsoft.Extensions.Options": "[7.0.0, 8.0.0)",
-          "NSwag.Generation": "13.18.2"
+          "NSwag.Generation": "13.19.0"
         }
       },
       "NuGet.Frameworks": {
         "type": "Transitive",
-        "resolved": "5.11.0",
-        "contentHash": "eaiXkUjC4NPcquGWzAGMXjuxvLwc6XGKMptSyOGQeT0X70BUZObuybJFZLA0OfTdueLd3US23NBPTBb6iF3V1Q=="
+        "resolved": "6.5.0",
+        "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg=="
       },
       "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
         "type": "Transitive",
@@ -659,12 +659,8 @@
       },
       "SixLabors.ImageSharp": {
         "type": "Transitive",
-        "resolved": "2.1.3",
-        "contentHash": "8yonNRWX3vUE9k29ta0Hbfa0AEc0hbDjSH/nZ3vOTJEXmY6hLnGsjDKoz96Z+AgOsrdkAu6PdL/Ebaf70aitzw==",
-        "dependencies": {
-          "System.Runtime.CompilerServices.Unsafe": "5.0.0",
-          "System.Text.Encoding.CodePages": "5.0.0"
-        }
+        "resolved": "3.0.1",
+        "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg=="
       },
       "SQLitePCLRaw.bundle_e_sqlite3": {
         "type": "Transitive",
@@ -1133,11 +1129,6 @@
           "Microsoft.NETCore.Targets": "1.1.0"
         }
       },
-      "System.Runtime.CompilerServices.Unsafe": {
-        "type": "Transitive",
-        "resolved": "5.0.0",
-        "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
-      },
       "System.Runtime.Extensions": {
         "type": "Transitive",
         "resolved": "4.3.0",
@@ -1355,14 +1346,6 @@
           "System.Runtime": "4.3.0"
         }
       },
-      "System.Text.Encoding.CodePages": {
-        "type": "Transitive",
-        "resolved": "5.0.0",
-        "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
-        "dependencies": {
-          "Microsoft.NETCore.Platforms": "5.0.0"
-        }
-      },
       "System.Text.Encoding.Extensions": {
         "type": "Transitive",
         "resolved": "4.3.0",
@@ -1487,30 +1470,30 @@
       },
       "xunit.analyzers": {
         "type": "Transitive",
-        "resolved": "1.0.0",
-        "contentHash": "BeO8hEgs/c8Ls2647fPfieMngncvf0D0xYNDfIO59MolxtCtVjFRd6SRc+7tj8VMqkVOuJcnc9eh4ngI2cAmLQ=="
+        "resolved": "1.2.0",
+        "contentHash": "d3dehV/DASLRlR8stWQmbPPjfYC2tct50Evav+OlsJMkfFqkhYvzO1k0s81lk0px8O0knZU/FqC8SqbXOtn+hw=="
       },
       "xunit.assert": {
         "type": "Transitive",
-        "resolved": "2.4.2",
-        "contentHash": "pxJISOFjn2XTTi1mcDCkRZrTFb9OtRRCtx2kZFNF51GdReLr1ls2rnyxvAS4JO247K3aNtflvh5Q0346K5BROA==",
+        "resolved": "2.5.0",
+        "contentHash": "wN84pKX5jzfpgJ0bB6arrCA/oelBeYLCpnQ9Wj5xGEVPydKzVSDY5tEatFLHE/rO0+0RC+I4H5igGE118jRh1w==",
         "dependencies": {
           "NETStandard.Library": "1.6.1"
         }
       },
       "xunit.core": {
         "type": "Transitive",
-        "resolved": "2.4.2",
-        "contentHash": "KB4yGCxNqIVyekhJLXtKSEq6BaXVp/JO3mbGVE1hxypZTLEe7h+sTbAhpA+yZW2dPtXTuiW+C1B2oxxHEkrmOw==",
+        "resolved": "2.5.0",
+        "contentHash": "dnV0Mn2s1C0y2m33AylQyMkEyhBQsL4R0302kwSGiEGuY3JwzEmhTa9pnghyMRPliYSs4fXfkEAP+5bKXryGFg==",
         "dependencies": {
-          "xunit.extensibility.core": "[2.4.2]",
-          "xunit.extensibility.execution": "[2.4.2]"
+          "xunit.extensibility.core": "[2.5.0]",
+          "xunit.extensibility.execution": "[2.5.0]"
         }
       },
       "xunit.extensibility.core": {
         "type": "Transitive",
-        "resolved": "2.4.2",
-        "contentHash": "W1BoXTIN1C6kpVSMw25huSet25ky6IAQUNovu3zGOGN/jWnbgSoTyCrlIhmXSg0tH5nEf8q7h3OjNHOjyu5PfA==",
+        "resolved": "2.5.0",
+        "contentHash": "xRm6NIV3i7I+LkjsAJ91Xz2fxJm/oMEi2CYq1G5HlGTgcK1Zo2wNbLO6nKX1VG5FZzXibSdoLwr/MofVvh3mFA==",
         "dependencies": {
           "NETStandard.Library": "1.6.1",
           "xunit.abstractions": "2.0.3"
@@ -1518,11 +1501,11 @@
       },
       "xunit.extensibility.execution": {
         "type": "Transitive",
-        "resolved": "2.4.2",
-        "contentHash": "CZmgcKkwpyo8FlupZdWpJCryrAOWLh1FBPG6gmVZuPQkGQsim/oL4PcP4nfrC2hHgXUFtluvaJ0Sp9PQKUMNpg==",
+        "resolved": "2.5.0",
+        "contentHash": "7+v2Bvp+1ew1iMGQVb1glICi8jcNdHbRUX6Ru0dmJBViGdjiS7kyqcX2VxleQhFbKNi+WF0an7/TeTXD283RlQ==",
         "dependencies": {
           "NETStandard.Library": "1.6.1",
-          "xunit.extensibility.core": "[2.4.2]"
+          "xunit.extensibility.core": "[2.5.0]"
         }
       },
       "YamlDotNet": {
@@ -1534,14 +1517,14 @@
         "type": "Project",
         "dependencies": {
           "AutoMapper": "[12.0.1, )",
-          "AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.0, )",
-          "Markdig": "[0.30.4, )",
-          "Microsoft.AspNetCore.SpaServices.Extensions": "[7.0.3, )",
-          "Microsoft.EntityFrameworkCore": "[7.0.3, )",
-          "Microsoft.EntityFrameworkCore.Analyzers": "[7.0.3, )",
-          "Microsoft.EntityFrameworkCore.Sqlite": "[7.0.3, )",
-          "NSwag.AspNetCore": "[13.18.2, )",
-          "SixLabors.ImageSharp": "[2.1.3, )"
+          "AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
+          "Markdig": "[0.31.0, )",
+          "Microsoft.AspNetCore.SpaServices.Extensions": "[7.0.9, )",
+          "Microsoft.EntityFrameworkCore": "[7.0.9, )",
+          "Microsoft.EntityFrameworkCore.Analyzers": "[7.0.9, )",
+          "Microsoft.EntityFrameworkCore.Sqlite": "[7.0.9, )",
+          "NSwag.AspNetCore": "[13.19.0, )",
+          "SixLabors.ImageSharp": "[3.0.1, )"
         }
       }
     }
diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj
index 6000cdb5..92fc6dec 100644
--- a/BackEnd/Timeline/Timeline.csproj
+++ b/BackEnd/Timeline/Timeline.csproj
@@ -29,18 +29,18 @@
 
     <ItemGroup>
         <PackageReference Include="AutoMapper" Version="12.0.1" />
-        <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
-        <PackageReference Include="Markdig" Version="0.30.4" />
-        <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" />
-        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="7.0.3" />
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
+        <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
+        <PackageReference Include="Markdig" Version="0.31.0" />
+        <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.9" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="7.0.9" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9">
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
           <PrivateAssets>all</PrivateAssets>
         </PackageReference>
-        <PackageReference Include="NSwag.AspNetCore" Version="13.18.2" />
-        <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
+        <PackageReference Include="NSwag.AspNetCore" Version="13.19.0" />
+        <PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
     </ItemGroup>
 
     <ItemGroup>
diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json
index 3dc3a828..e6716f52 100644
--- a/BackEnd/Timeline/packages.lock.json
+++ b/BackEnd/Timeline/packages.lock.json
@@ -13,37 +13,37 @@
       },
       "AutoMapper.Extensions.Microsoft.DependencyInjection": {
         "type": "Direct",
-        "requested": "[12.0.0, )",
-        "resolved": "12.0.0",
-        "contentHash": "XCJ4E3oKrbRl1qY9Mr+7uyC0xZj1+bqQjmQRWTiTKiVuuXTny+7YFWHi20tPjwkMukLbicN6yGlDy5PZ4wyi1w==",
+        "requested": "[12.0.1, )",
+        "resolved": "12.0.1",
+        "contentHash": "+g/K+Vpe3gGMKGzjslMOdqNlkikScDjWfVvmWTayrDHaG/n2pPmFBMa+jKX1r/h6BDGFdkyRjAuhFE3ykW+r1g==",
         "dependencies": {
-          "AutoMapper": "12.0.0",
+          "AutoMapper": "[12.0.1]",
           "Microsoft.Extensions.Options": "6.0.0"
         }
       },
       "Markdig": {
         "type": "Direct",
-        "requested": "[0.30.4, )",
-        "resolved": "0.30.4",
-        "contentHash": "9LL7Mzubwf6QmOTZIqf+vERiinqPA1H+irBA38POhr93W/it00AvFWaCtF80+8VJTooFmcAJyZT2f60t1f88eQ=="
+        "requested": "[0.31.0, )",
+        "resolved": "0.31.0",
+        "contentHash": "CEdZ3KyFfz5IPMrofhCOSrc4O2KNvSXSMXt3fv0W5AFCsk6a/JhKZuTBxGSadNt8n4zyyd/xXfoUMvr4ZbZ4Fw=="
       },
       "Microsoft.AspNetCore.SpaServices.Extensions": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "dh2ZmM+4Y3b3pCBndGuvpBIp1OOOK+CeZruphDKoFWgmxfN/RYmV0QvOS3u1McVb1yFmcVcXgVx0DEW5+WRkFg==",
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "qPuCTxQD832rs6vH5W8nVowU+BiWh45uH8+WnhEFiqgjGAGO8gw9Yf2aO4m0+VBi60kLwM8KYwYyLlpm0A8tbA==",
         "dependencies": {
           "Microsoft.Extensions.FileProviders.Physical": "7.0.0"
         }
       },
       "Microsoft.EntityFrameworkCore": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "6XUI2YGoaLMoP9KGaqWmmd4B2n5bpQbXrVRpH20Et3YjQ0Rn3Ia6HM/ANcSq9rBfjfUySgo9SwUZgQ4m4PF3LQ==",
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "9YuCdQWuRAmYYHqwW1h5ukKMC1fmNvcVHdp3gb8zdHxwSQz7hkGpYOBEjm6dRXRmGRkpUyHL8rwUz4kd53Ev0A==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore.Abstractions": "7.0.3",
-          "Microsoft.EntityFrameworkCore.Analyzers": "7.0.3",
+          "Microsoft.EntityFrameworkCore.Abstractions": "7.0.9",
+          "Microsoft.EntityFrameworkCore.Analyzers": "7.0.9",
           "Microsoft.Extensions.Caching.Memory": "7.0.0",
           "Microsoft.Extensions.DependencyInjection": "7.0.0",
           "Microsoft.Extensions.Logging": "7.0.0"
@@ -51,53 +51,49 @@
       },
       "Microsoft.EntityFrameworkCore.Analyzers": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "CLyRWFLwaOUZNPEia/aBMzFxZqm/ITKt3B+yUFtrg4Ys5VF3n2gvneuItC9IhpeOcjfdSgu/yUKf8y/IsNHs5A=="
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "VvqFD3DdML6LhPCAR/lG2xNX66juylC8v57yUAAYnUSdEUOMRi3lNoT4OrNdG0rere3UOQPhvVl5FH2QdyoF8Q=="
       },
       "Microsoft.EntityFrameworkCore.Sqlite": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "bNWgYJsT6+0I98RSIY0rZAYu6NkLFgPvWPO1LKTsBqG6iwhCW3M38JGDVOOSf69N9HUZz9ilKNLTyYKNRQkyGw==",
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "otP72XqfG9UdO0+3O2p1UEx4MLXSAulXc8tIvuq+v1KPtBj/GuMZeJIz+eCu09Xv6fJ6k7aUb8J/x+H0rqOjjA==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore.Sqlite.Core": "7.0.3",
+          "Microsoft.EntityFrameworkCore.Sqlite.Core": "7.0.9",
           "SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
         }
       },
       "Microsoft.EntityFrameworkCore.Tools": {
         "type": "Direct",
-        "requested": "[7.0.3, )",
-        "resolved": "7.0.3",
-        "contentHash": "yHFlYPZS3Jx7JMCQnGKfJzv95rJWVcmcUn/OW5cbCyWgQk81JJpTZ9Q6kkvwquYjFRfvYHBGuXNIYhAJokOBTQ==",
+        "requested": "[7.0.9, )",
+        "resolved": "7.0.9",
+        "contentHash": "GWOSIe8ltR/mqv2wpRvMhy6ULapdKhZXXpXSGWzG1fRxhYXjSGpe2Pqhxqo46o9RDGlk0WJUXrwyU+N4voPxMw==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore.Design": "7.0.3"
+          "Microsoft.EntityFrameworkCore.Design": "7.0.9"
         }
       },
       "NSwag.AspNetCore": {
         "type": "Direct",
-        "requested": "[13.18.2, )",
-        "resolved": "13.18.2",
-        "contentHash": "ueewpltbJpZGWgR3BkLXQRX1g7ZtzNl7sT5DI90lNufJhLWnjC5nAfxRTP4J8nqO1vR79zLYzzA5ZwsusZaTpA==",
+        "requested": "[13.19.0, )",
+        "resolved": "13.19.0",
+        "contentHash": "7ZPAHrJCMPuDN3kE5bZIckWIVtorMj3YCU0bYu83CqVBsOGlxUy6VS8CnmLHlmAJ5gxEA+RD+KGepJvk3u+ygQ==",
         "dependencies": {
           "Microsoft.Extensions.ApiDescription.Server": "6.0.3",
           "Microsoft.Extensions.FileProviders.Embedded": "[7.0.0, 8.0.0)",
-          "NSwag.Annotations": "13.18.2",
-          "NSwag.Core": "13.18.2",
-          "NSwag.Core.Yaml": "13.18.2",
-          "NSwag.Generation": "13.18.2",
-          "NSwag.Generation.AspNetCore": "13.18.2"
+          "NSwag.Annotations": "13.19.0",
+          "NSwag.Core": "13.19.0",
+          "NSwag.Core.Yaml": "13.19.0",
+          "NSwag.Generation": "13.19.0",
+          "NSwag.Generation.AspNetCore": "13.19.0"
         }
       },
       "SixLabors.ImageSharp": {
         "type": "Direct",
-        "requested": "[2.1.3, )",
-        "resolved": "2.1.3",
-        "contentHash": "8yonNRWX3vUE9k29ta0Hbfa0AEc0hbDjSH/nZ3vOTJEXmY6hLnGsjDKoz96Z+AgOsrdkAu6PdL/Ebaf70aitzw==",
-        "dependencies": {
-          "System.Runtime.CompilerServices.Unsafe": "5.0.0",
-          "System.Text.Encoding.CodePages": "5.0.0"
-        }
+        "requested": "[3.0.1, )",
+        "resolved": "3.0.1",
+        "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg=="
       },
       "Humanizer.Core": {
         "type": "Transitive",
@@ -111,44 +107,44 @@
       },
       "Microsoft.Data.Sqlite.Core": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "pCmzLLWTIrIv94o7JtQ1qcPD0oc1YNY9XvlO6/tOF9YCcUfDZ3Tx9Z//CM7hFnprduHFPekif7jteBc/sXQ31Q==",
+        "resolved": "7.0.9",
+        "contentHash": "ow2PPoeW0yFc7NhexacQUw/LVjkO1mLK3VZAxhVIVjmQWlgYl/4mo9/U7uz+z75I+ZN6LUvq9M0ftU3IE75Ilg==",
         "dependencies": {
           "SQLitePCLRaw.core": "2.1.4"
         }
       },
       "Microsoft.EntityFrameworkCore.Abstractions": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "0NaFBZykUlQHknddRuKY4v+MFZX/AW+v2V85dgj7abIlt+kL3GWa7QNH5S1084VLf1u+dq1SnhZsOvykc3Y0sA=="
+        "resolved": "7.0.9",
+        "contentHash": "cfY6Fn7cnP/5pXndL8QYaey/B2nGn1fwze5aSaMJymmbqwqYzFiszHuWbsdVBCDYJc8ok7eB1m/nCc3/rltulQ=="
       },
       "Microsoft.EntityFrameworkCore.Design": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "Nv0Y2Zh8d919qKq8Q1bvbWQbFeb4JQ7jCuakajSVtip5JIwt4hTIWetVIapJ2vOQDDZuAHCzkjimMOlHH5LVsQ==",
+        "resolved": "7.0.9",
+        "contentHash": "tWsa+spzKZAwHrUP6vYM1LLh0P89UMcldEjerFPPZb0LcI/ONQmh7ldK4Q8TeRuIiuXxYgRYPElSgxwLp14oug==",
         "dependencies": {
           "Humanizer.Core": "2.14.1",
-          "Microsoft.EntityFrameworkCore.Relational": "7.0.3",
+          "Microsoft.EntityFrameworkCore.Relational": "7.0.9",
           "Microsoft.Extensions.DependencyModel": "7.0.0",
           "Mono.TextTemplating": "2.2.1"
         }
       },
       "Microsoft.EntityFrameworkCore.Relational": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "RxNNjtTrxsMtdBtgoXGRSy8uCXaBHaVzIonTeo7+Ys+N0yEWwhf2E74cxneyunMi13Ezlld10ecCHlDubEU/Pw==",
+        "resolved": "7.0.9",
+        "contentHash": "u7iN6cNd6SJUlpdk24JVIbkji/UbkEEQ7pXncTyT4eXXj+Hz2y4NSZFOAywPGcioIgX1YzbKWDiJhk7hjSFxBQ==",
         "dependencies": {
-          "Microsoft.EntityFrameworkCore": "7.0.3",
+          "Microsoft.EntityFrameworkCore": "7.0.9",
           "Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
         }
       },
       "Microsoft.EntityFrameworkCore.Sqlite.Core": {
         "type": "Transitive",
-        "resolved": "7.0.3",
-        "contentHash": "w0yzGr1IDY4sp+RkFnVlWVF1N2lmaJHCmzVldZ/e36bXdT95E8N7/+JYWwKm1nLWEpS6naaoNtqQO6buRHTfAg==",
+        "resolved": "7.0.9",
+        "contentHash": "uLy1swPL4AD6BQsu3SEIzSWKc6gIlmoSAYZX39VnbrqvzkGmMsgEgdCsJIZvdjAW/vzhPazACKMYna1iif04sg==",
         "dependencies": {
-          "Microsoft.Data.Sqlite.Core": "7.0.3",
-          "Microsoft.EntityFrameworkCore.Relational": "7.0.3",
+          "Microsoft.Data.Sqlite.Core": "7.0.9",
+          "Microsoft.EntityFrameworkCore.Relational": "7.0.9",
           "Microsoft.Extensions.DependencyModel": "7.0.0"
         }
       },
@@ -270,8 +266,8 @@
       },
       "Microsoft.NETCore.Platforms": {
         "type": "Transitive",
-        "resolved": "5.0.0",
-        "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
+        "resolved": "1.1.0",
+        "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
       },
       "Microsoft.NETCore.Targets": {
         "type": "Transitive",
@@ -288,8 +284,8 @@
       },
       "Namotion.Reflection": {
         "type": "Transitive",
-        "resolved": "2.1.0",
-        "contentHash": "9t63RauDp+CWzMCcCRAGXLRqEVIw0djYisGaDWhgHuXSaz/Djjpp9gpumCWVLpuDHLNf4HUmYWJeBt4AUyJSWA==",
+        "resolved": "2.1.2",
+        "contentHash": "7tSHAzX8GWKy0qrW6OgQWD7kAZiqzhq+m1503qczuwuK6ZYhOGCQUxw+F3F4KkRM70aB6RMslsRVSCFeouIehw==",
         "dependencies": {
           "Microsoft.CSharp": "4.3.0"
         }
@@ -329,65 +325,65 @@
       },
       "NJsonSchema": {
         "type": "Transitive",
-        "resolved": "10.8.0",
-        "contentHash": "lChjsLWaxyvElh4WJjVhdIiCtx7rimYGFTxtSi2pAkZf0ZnKaXYIX484HCVyzbDDHejDZPgOrcfAJ3kqNSTONw==",
+        "resolved": "10.9.0",
+        "contentHash": "IBPo6Srxn2MEcIFM3HdM4QImrJbsIeujENQyzHL2Pv6wLsKSYAyAEilecRqaLOhoy3snEiPLx7hhv7opbhOxKQ==",
         "dependencies": {
-          "Namotion.Reflection": "2.1.0",
+          "Namotion.Reflection": "2.1.2",
           "Newtonsoft.Json": "9.0.1"
         }
       },
       "NJsonSchema.Yaml": {
         "type": "Transitive",
-        "resolved": "10.8.0",
-        "contentHash": "sygi7CN1AQHFNfpk/RRgsAMl+5tTM65Rajau3QPKoflJoZbxPQLBgZZrH7SMINumkW61JqDy6EhKfHsxngTzqw==",
+        "resolved": "10.9.0",
+        "contentHash": "lGOtsydEnX1mVLT2Cyre7mST7K7crveYtai9FGC2i7COKEMEFjsfbnUtgXSHKxS/bzxVEIjLa49tR6xw34WkEw==",
         "dependencies": {
           "Microsoft.CSharp": "4.4.1",
-          "NJsonSchema": "10.8.0",
+          "NJsonSchema": "10.9.0",
           "YamlDotNet": "11.2.1"
         }
       },
       "NSwag.Annotations": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "2zDLZXEU1PVuvdMc1k+SIkLonp8vPRHAkto6qQfCeCPqhDaF82+FOWe3TLJIYBS8IC7aV/mT+eoqJLeZvXiN4A=="
+        "resolved": "13.19.0",
+        "contentHash": "ukpuIdZTczGUwyIAz954xiefikAUShtEoColOODeKilKGcB5CXn1Q6j3D/Er75z2+qMA3qieXb386xAyW2/v+Q=="
       },
       "NSwag.Core": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "QM6OnVtUkD62fi0lgFF65KbZ0Y8ToGD+Rb4W3Nz6ymCWWfVbMwkn8xUxIjWSLsOEYJCbLvBBeGt+oBNrJwxb+A==",
+        "resolved": "13.19.0",
+        "contentHash": "UkPCsEJ5WaE6mcEjarZZfMPyyRPHOWK9YuFvxNjrSHclCxVUZ7jgFtNGMX0wy0WvXkmA5b6H9IoaAJECMxcu/g==",
         "dependencies": {
-          "NJsonSchema": "10.8.0",
+          "NJsonSchema": "10.9.0",
           "Newtonsoft.Json": "10.0.1"
         }
       },
       "NSwag.Core.Yaml": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "y+h+4jrN3BC0B3v1Qg0Fr3jrGTB5nR5RAn5ZG5qBveiazeB9VDfR5cP9URN6wb3VypahHRzYryUB2KFZuiS22A==",
+        "resolved": "13.19.0",
+        "contentHash": "F97HsEclpnydInq1Wrd74WDQePHQ7S+e4yKi4BiYi/hLy6s88YlpRFB3R5Bx64iRkGKesU/uGiovKtFevpW2Bw==",
         "dependencies": {
-          "NJsonSchema.Yaml": "10.8.0",
-          "NSwag.Core": "13.18.2",
+          "NJsonSchema.Yaml": "10.9.0",
+          "NSwag.Core": "13.19.0",
           "YamlDotNet": "11.2.1"
         }
       },
       "NSwag.Generation": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "hmdi+kXwKTgxu2aYgaILDw2nKvco1/wEd1RXaVNIz4ck6yHTJ58ANVcUjN3ytmL//f+hKEKWZznTi4YQW7jtpQ==",
+        "resolved": "13.19.0",
+        "contentHash": "TdBY+P6WGElaHIAzYaT9te141y46UCUTjlGgGKi5eR4ZXHE/JhmrXXU8kr3zFHWw7gk6TeOKy5LnPlJaxNXb5Q==",
         "dependencies": {
-          "NJsonSchema": "10.8.0",
-          "NSwag.Core": "13.18.2",
+          "NJsonSchema": "10.9.0",
+          "NSwag.Core": "13.19.0",
           "Newtonsoft.Json": "10.0.1"
         }
       },
       "NSwag.Generation.AspNetCore": {
         "type": "Transitive",
-        "resolved": "13.18.2",
-        "contentHash": "D+yfRbXtv/wDan1oEkskGWejlCiUTdLlV93qynay1Xp1HmnC0XYspjLkfMGr9kqYX4CY9SrMUnDUsRNeyOoR/g==",
+        "resolved": "13.19.0",
+        "contentHash": "1ne3rqJtWTUbZuN/kyCTk1cT36ZLXZs2/tE6Kvhb8MA6GySbwSwaLIJ1eHFn6dksp5+Ic5EhI1ZL6wrqapQgsg==",
         "dependencies": {
           "Microsoft.Extensions.DependencyInjection.Abstractions": "[7.0.0, 8.0.0)",
           "Microsoft.Extensions.Options": "[7.0.0, 8.0.0)",
-          "NSwag.Generation": "13.18.2"
+          "NSwag.Generation": "13.19.0"
         }
       },
       "SQLitePCLRaw.bundle_e_sqlite3": {
@@ -750,11 +746,6 @@
           "Microsoft.NETCore.Targets": "1.1.0"
         }
       },
-      "System.Runtime.CompilerServices.Unsafe": {
-        "type": "Transitive",
-        "resolved": "5.0.0",
-        "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
-      },
       "System.Runtime.Extensions": {
         "type": "Transitive",
         "resolved": "4.3.0",
@@ -830,14 +821,6 @@
           "System.Runtime": "4.3.0"
         }
       },
-      "System.Text.Encoding.CodePages": {
-        "type": "Transitive",
-        "resolved": "5.0.0",
-        "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
-        "dependencies": {
-          "Microsoft.NETCore.Platforms": "5.0.0"
-        }
-      },
       "System.Text.Encoding.Extensions": {
         "type": "Transitive",
         "resolved": "4.3.0",
diff --git a/FrontEnd/.eslintignore b/FrontEnd/.eslintignore
index 6fc7bee6..19d1b61c 100644
--- a/FrontEnd/.eslintignore
+++ b/FrontEnd/.eslintignore
@@ -1 +1,2 @@
 dist
+src/migrating
diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx
index ca3e4d38..6029daac 100644
--- a/FrontEnd/src/App.tsx
+++ b/FrontEnd/src/App.tsx
@@ -10,8 +10,6 @@ import LoginPage from "./pages/login";
 import RegisterPage from "./pages/register";
 import TimelinePage from "./pages/timeline";
 import LoadingPage from "./pages/loading";
-import Search from "./views/search";
-import Admin from "./views/admin";
 import AlertHost from "./views/common/alert/AlertHost";
 
 export default function App() {
@@ -25,8 +23,6 @@ export default function App() {
           <Route path="register" element={<RegisterPage />} />
           <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 />} />
           <Route path=":owner/:timeline" element={<TimelinePage />} />
           <Route path="" element={<HomePage />} />
diff --git a/FrontEnd/src/migrating/admin/Admin.tsx b/FrontEnd/src/migrating/admin/Admin.tsx
new file mode 100644
index 00000000..986c36b4
--- /dev/null
+++ b/FrontEnd/src/migrating/admin/Admin.tsx
@@ -0,0 +1,27 @@
+import { Route, Routes } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+
+import AdminNav from "./AdminNav";
+import UserAdmin from "./UserAdmin";
+import MoreAdmin from "./MoreAdmin";
+
+import "./index.css";
+
+const Admin: React.FC = () => {
+  useTranslation("admin");
+
+  return (
+    <>
+      <div className="container">
+        <AdminNav className="mt-2" />
+        <Routes>
+          <Route index element={<UserAdmin />} />
+          <Route path="user" element={<UserAdmin />} />
+          <Route path="more" element={<MoreAdmin />} />
+        </Routes>
+      </div>
+    </>
+  );
+};
+
+export default Admin;
diff --git a/FrontEnd/src/migrating/admin/AdminNav.tsx b/FrontEnd/src/migrating/admin/AdminNav.tsx
new file mode 100644
index 00000000..b7385e5c
--- /dev/null
+++ b/FrontEnd/src/migrating/admin/AdminNav.tsx
@@ -0,0 +1,29 @@
+import { useLocation } from "react-router-dom";
+
+import Tabs from "../common/tab/Tabs";
+
+export function AdminNav({ className }: { className?: string }) {
+  const location = useLocation();
+  const name = location.pathname.split("/")[2] ?? "user";
+
+  return (
+    <Tabs
+      className={className}
+      activeTabName={name}
+      tabs={[
+        {
+          name: "user",
+          text: "admin:nav.users",
+          link: "/admin/user",
+        },
+        {
+          name: "more",
+          text: "admin:nav.more",
+          link: "/admin/more",
+        },
+      ]}
+    />
+  );
+}
+
+export default AdminNav;
diff --git a/FrontEnd/src/migrating/admin/MoreAdmin.tsx b/FrontEnd/src/migrating/admin/MoreAdmin.tsx
new file mode 100644
index 00000000..d49d211f
--- /dev/null
+++ b/FrontEnd/src/migrating/admin/MoreAdmin.tsx
@@ -0,0 +1,7 @@
+import * as React from "react";
+
+const MoreAdmin: React.FC = () => {
+  return <>More...</>;
+};
+
+export default MoreAdmin;
diff --git a/FrontEnd/src/migrating/admin/UserAdmin.tsx b/FrontEnd/src/migrating/admin/UserAdmin.tsx
new file mode 100644
index 00000000..08560c87
--- /dev/null
+++ b/FrontEnd/src/migrating/admin/UserAdmin.tsx
@@ -0,0 +1,301 @@
+// eslint-disable
+// @ts-nocheck
+
+import { useState, useEffect } from "react";
+import * as React from "react";
+import { Trans, useTranslation } from "react-i18next";
+import classnames from "classnames";
+
+import { getHttpUserClient, HttpUser, kUserPermissionList } from "@/http/user";
+
+import OperationDialog from "../common/dialog/OperationDialog";
+import Button from "../common/button/Button";
+import Spinner from "../common/Spinner";
+import FlatButton from "../common/button/FlatButton";
+import IconButton from "../common/button/IconButton";
+
+const CreateUserDialog: React.FC<{
+  open: boolean;
+  close: () => void;
+  onSuccess: (user: HttpUser) => void;
+}> = ({ open, close, onSuccess }) => {
+  return (
+    <OperationDialog
+      title="admin:user.dialog.create.title"
+      inputPrompt="admin:user.dialog.create.prompt"
+      inputs={[
+        { key: "username", type: "text", label: "admin:user.username" },
+        { key: "password", type: "text", label: "admin:user.password" },
+      ]}
+      onProcess={({ username, password }) =>
+        getHttpUserClient().post({
+          username: username as string,
+          password: password as string,
+        })
+      }
+      onClose={close}
+      open={open}
+      onSuccessAndClose={onSuccess}
+    />
+  );
+};
+
+const UsernameLabel: React.FC<{ children: React.ReactNode }> = (props) => {
+  return <span style={{ color: "blue" }}>{props.children}</span>;
+};
+
+const UserDeleteDialog: React.FC<{
+  open: boolean;
+  close: () => void;
+  user: HttpUser;
+  onSuccess: () => void;
+}> = ({ open, close, user, onSuccess }) => {
+  return (
+    <OperationDialog
+      open={open}
+      onClose={close}
+      title="admin:user.dialog.delete.title"
+      themeColor="danger"
+      inputPrompt={() => (
+        <Trans i18nKey="user.dialog.delete.prompt" ns="admin">
+          0<UsernameLabel>{user.username}</UsernameLabel>2
+        </Trans>
+      )}
+      onProcess={() => getHttpUserClient().delete(user.username)}
+      onSuccessAndClose={onSuccess}
+    />
+  );
+};
+
+const UserModifyDialog: React.FC<{
+  open: boolean;
+  close: () => void;
+  user: HttpUser;
+  onSuccess: () => void;
+}> = ({ open, close, user, onSuccess }) => {
+  return (
+    <OperationDialog
+      open={open}
+      onClose={close}
+      title="admin:user.dialog.modify.title"
+      inputPromptNode={
+        <Trans i18nKey="admin:user.dialog.modify.prompt">
+          0<UsernameLabel>{user.username}</UsernameLabel>2
+        </Trans>
+      }
+      inputs={
+        [
+          {
+            type: "text",
+            label: "admin:user.username",
+            initValue: user.username,
+          },
+          { type: "text", label: "admin:user.password" },
+          {
+            type: "text",
+            label: "admin:user.nickname",
+            initValue: user.nickname,
+          },
+        ] as const
+      }
+      onProcess={([username, password, nickname]) =>
+        getHttpUserClient().patch(user.username, {
+          username: username !== user.username ? username : undefined,
+          password: password !== "" ? password : undefined,
+          nickname: nickname !== user.nickname ? nickname : undefined,
+        })
+      }
+      onSuccessAndClose={onSuccess}
+    />
+  );
+};
+
+const UserPermissionModifyDialog: React.FC<{
+  open: boolean;
+  close: () => void;
+  user: HttpUser;
+  onSuccess: () => void;
+}> = ({ open, close, user, onSuccess }) => {
+  const oldPermissionBoolList: boolean[] = kUserPermissionList.map(
+    (permission) => user.permissions.includes(permission),
+  );
+
+  return (
+    <OperationDialog
+      open={open}
+      onClose={close}
+      title="admin:user.dialog.modifyPermissions.title"
+      themeColor="danger"
+      inputPrompt={() => (
+        <Trans i18nKey="admin:user.dialog.modifyPermissions.prompt">
+          0<UsernameLabel>{user.username}</UsernameLabel>2
+        </Trans>
+      )}
+      inputScheme={kUserPermissionList.map<OperationDialogBoolInput>(
+        (permission, index) => ({
+          type: "bool",
+          label: { type: "custom", value: permission },
+          initValue: oldPermissionBoolList[index],
+        }),
+      )}
+      onProcess={async (newPermissionBoolList): Promise<boolean[]> => {
+        for (let index = 0; index < kUserPermissionList.length; index++) {
+          const oldValue = oldPermissionBoolList[index];
+          const newValue = newPermissionBoolList[index];
+          const permission = kUserPermissionList[index];
+          if (oldValue === newValue) continue;
+          if (newValue) {
+            await getHttpUserClient().putUserPermission(
+              user.username,
+              permission,
+            );
+          } else {
+            await getHttpUserClient().deleteUserPermission(
+              user.username,
+              permission,
+            );
+          }
+        }
+        return newPermissionBoolList;
+      }}
+      onSuccessAndClose={onSuccess}
+    />
+  );
+};
+
+interface UserItemProps {
+  user: HttpUser;
+  onChange: () => void;
+}
+
+const UserItem: React.FC<UserItemProps> = ({ user, onChange }) => {
+  const { t } = useTranslation();
+
+  const [dialog, setDialog] = useState<
+    "delete" | "modify" | "permission" | null
+  >(null);
+
+  const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false);
+
+  return (
+    <>
+      <div className="admin-user-item">
+        <IconButton
+          icon="pencil-square"
+          className="cru-float-right"
+          onClick={() => setEditMaskVisible(true)}
+        />
+        <h5 className="cru-color-primary">{user.username}</h5>
+        <small className="d-block cru-color-secondary">
+          {t("admin:user.nickname")}
+          {user.nickname}
+        </small>
+        <small className="d-block cru-color-secondary">
+          {t("admin:user.uniqueId")}
+          {user.uniqueId}
+        </small>
+        <small className="d-block cru-color-secondary">
+          {t("admin:user.permissions")}
+          {user.permissions.map((permission) => {
+            return (
+              <span key={permission} className="cru-color-danger">
+                {permission}
+              </span>
+            );
+          })}
+        </small>
+        <div
+          className={classnames("edit-mask", !editMaskVisible && "d-none")}
+          onClick={() => setEditMaskVisible(false)}
+        >
+          <FlatButton
+            text="admin:user.modify"
+            onClick={() => setDialog("modify")}
+          />
+          <FlatButton
+            text="admin:user.modifyPermissions"
+            onClick={() => setDialog("permission")}
+          />
+          <FlatButton
+            text="admin:user.delete"
+            color="danger"
+            onClick={() => setDialog("delete")}
+          />
+        </div>
+      </div>
+      <UserDeleteDialog
+        open={dialog === "delete"}
+        close={() => setDialog(null)}
+        user={user}
+        onSuccess={onChange}
+      />
+      <UserModifyDialog
+        open={dialog === "modify"}
+        close={() => setDialog(null)}
+        user={user}
+        onSuccess={onChange}
+      />
+      <UserPermissionModifyDialog
+        open={dialog === "permission"}
+        close={() => setDialog(null)}
+        user={user}
+        onSuccess={onChange}
+      />
+    </>
+  );
+};
+
+const UserAdmin: React.FC = () => {
+  const [users, setUsers] = useState<HttpUser[] | null>(null);
+  const [dialog, setDialog] = useState<"create" | null>(null);
+  const [usersVersion, setUsersVersion] = useState<number>(0);
+  const updateUsers = (): void => {
+    setUsersVersion(usersVersion + 1);
+  };
+
+  useEffect(() => {
+    let subscribe = true;
+    void getHttpUserClient()
+      .list()
+      .then((us) => {
+        if (subscribe) {
+          setUsers(us.items);
+        }
+      });
+    return () => {
+      subscribe = false;
+    };
+  }, [usersVersion]);
+
+  if (users) {
+    const userComponents = users.map((user) => {
+      return (
+        <UserItem key={user.username} user={user} onChange={updateUsers} />
+      );
+    });
+
+    return (
+      <>
+        <div className="row justify-content-end my-2">
+          <div className="col col-auto">
+            <Button
+              text="admin:create"
+              color="success"
+              onClick={() => setDialog("create")}
+            />
+          </div>
+        </div>
+        {userComponents}
+        <CreateUserDialog
+          open={dialog === "create"}
+          close={() => setDialog(null)}
+          onSuccess={updateUsers}
+        />
+      </>
+    );
+  } else {
+    return <Spinner />;
+  }
+};
+
+export default UserAdmin;
diff --git a/FrontEnd/src/migrating/admin/index.css b/FrontEnd/src/migrating/admin/index.css
new file mode 100644
index 00000000..17e24586
--- /dev/null
+++ b/FrontEnd/src/migrating/admin/index.css
@@ -0,0 +1,33 @@
+.admin-user-item {
+  position: relative;
+  border: var(--cru-primary-color) solid;
+  border-width: 1px 1px 0;
+  padding: 1em;
+}
+
+.admin-user-item:last-of-type {
+  border-bottom-width: 1px;
+}
+
+.admin-user-item .edit-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: rgba(255, 255, 255, 0.9);
+  position: absolute;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+}
+
+@media (max-width: 576px) {
+  .admin-user-item .edit-mask {
+    flex-direction: column;
+  }
+}
+
+.admin-user-item .edit-mask button {
+  margin: 0.5em 2em;
+}
diff --git a/FrontEnd/src/migrating/admin/index.tsx b/FrontEnd/src/migrating/admin/index.tsx
new file mode 100644
index 00000000..0467711d
--- /dev/null
+++ b/FrontEnd/src/migrating/admin/index.tsx
@@ -0,0 +1,7 @@
+import { lazy } from "react";
+
+const Admin = lazy(
+  () => import(/* webpackChunkName: "admin" */ "./Admin")
+);
+
+export default Admin;
diff --git a/FrontEnd/src/migrating/center/CenterBoards.tsx b/FrontEnd/src/migrating/center/CenterBoards.tsx
new file mode 100644
index 00000000..a8be2c29
--- /dev/null
+++ b/FrontEnd/src/migrating/center/CenterBoards.tsx
@@ -0,0 +1,131 @@
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+
+import { highlightTimelineUsername } from "@/common";
+
+import { pushAlert } from "@/services/alert";
+import { useUserLoggedIn } from "@/services/user";
+
+import { getHttpTimelineClient } from "@/http/timeline";
+import { getHttpBookmarkClient } from "@/http/bookmark";
+
+import TimelineBoard from "./TimelineBoard";
+
+const CenterBoards: React.FC = () => {
+  const { t } = useTranslation();
+
+  const user = useUserLoggedIn();
+
+  return (
+    <>
+      <div className="row justify-content-center">
+        <div className="col col-12 col-md-6">
+          <div className="row">
+            <div className="col col-12 my-2">
+              <TimelineBoard
+                title={t("home.bookmarkTimeline")}
+                load={() =>
+                  getHttpBookmarkClient()
+                    .list(user.username)
+                    .then((p) => p.items)
+                }
+                editHandler={{
+                  onDelete: (owner, timeline) => {
+                    return getHttpBookmarkClient()
+                      .delete(user.username, owner, timeline)
+                      .catch((e) => {
+                        pushAlert({
+                          message: "home.message.deleteBookmarkFail",
+                          type: "danger",
+                        });
+                        throw e;
+                      });
+                  },
+                  onMove: (owner, timeline, index, offset) => {
+                    return getHttpBookmarkClient()
+                      .move(
+                        user.username,
+                        owner,
+                        timeline,
+                        index + offset + 1 // +1 because backend contract: index starts at 1
+                      )
+                      .catch((e) => {
+                        pushAlert({
+                          message: "home.message.moveBookmarkFail",
+                          type: "danger",
+                        });
+                        throw e;
+                      })
+                      .then();
+                  },
+                }}
+              />
+            </div>
+            <div className="col col-12 my-2">
+              <TimelineBoard
+                title={t("home.highlightTimeline")}
+                load={() =>
+                  getHttpBookmarkClient()
+                    .list(highlightTimelineUsername)
+                    .then((p) => p.items)
+                }
+                editHandler={
+                  user.username === highlightTimelineUsername
+                    ? {
+                        onDelete: (owner, timeline) => {
+                          return getHttpBookmarkClient()
+                            .delete(highlightTimelineUsername, owner, timeline)
+                            .catch((e) => {
+                              pushAlert({
+                                message: "home.message.deleteHighlightFail",
+                                type: "danger",
+                              });
+                              throw e;
+                            });
+                        },
+                        onMove: (owner, timeline, index, offset) => {
+                          return getHttpBookmarkClient()
+                            .move(
+                              highlightTimelineUsername,
+                              owner,
+                              timeline,
+                              index + offset + 1 // +1 because backend contract: index starts at 1
+                            )
+                            .catch((e) => {
+                              pushAlert({
+                                message: "home.message.moveBookmarkFail",
+                                type: "danger",
+                              });
+                              throw e;
+                            })
+                            .then();
+                        },
+                      }
+                    : undefined
+                }
+              />
+            </div>
+          </div>
+        </div>
+        <div className="col-12 col-md-6 my-2">
+          <TimelineBoard
+            title={t("home.relatedTimeline")}
+            load={() =>
+              getHttpTimelineClient()
+                .listTimeline({ relate: user.username })
+                .then((l) =>
+                  l.map((t, index) => ({
+                    timelineOwner: t.owner.username,
+                    timelineName: t.nameV2,
+                    position: index + 1,
+                  }))
+                )
+            }
+          />
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default CenterBoards;
diff --git a/FrontEnd/src/migrating/center/TimelineBoard.tsx b/FrontEnd/src/migrating/center/TimelineBoard.tsx
new file mode 100644
index 00000000..b3ccdf8c
--- /dev/null
+++ b/FrontEnd/src/migrating/center/TimelineBoard.tsx
@@ -0,0 +1,390 @@
+import * as React from "react";
+import classnames from "classnames";
+import { Link } from "react-router-dom";
+
+import { TimelineBookmark } from "@/http/bookmark";
+
+import TimelineLogo from "../common/TimelineLogo";
+import LoadFailReload from "../common/LoadFailReload";
+import FlatButton from "../common/button/FlatButton";
+import Card from "../common/Card";
+import Spinner from "../common/Spinner";
+import IconButton from "../common/button/IconButton";
+
+interface TimelineBoardItemProps {
+  timeline: TimelineBookmark;
+  // In height.
+  offset?: number;
+  // In px.
+  arbitraryOffset?: number;
+  // If not null, will disable navigation on click.
+  actions?: {
+    onDelete: () => void;
+    onMove: {
+      start: (e: React.PointerEvent) => void;
+      moving: (e: React.PointerEvent) => void;
+      end: (e: React.PointerEvent) => void;
+    };
+  };
+}
+
+const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({
+  timeline,
+  arbitraryOffset,
+  offset,
+  actions,
+}) => {
+  const content = (
+    <>
+      <TimelineLogo className="icon" />
+      <span className="title">
+        {timeline.timelineOwner}/{timeline.timelineName}
+      </span>
+      <span className="flex-grow-1"></span>
+      {actions != null ? (
+        <div className="right">
+          <IconButton
+            icon="trash"
+            color="danger"
+            className="px-2"
+            onClick={actions.onDelete}
+          />
+          <IconButton
+            icon="grip-vertical"
+            className="px-2 touch-action-none"
+            onPointerDown={(e) => {
+              e.currentTarget.setPointerCapture(e.pointerId);
+              actions.onMove.start(e);
+            }}
+            onPointerUp={(e) => {
+              actions.onMove.end(e);
+              try {
+                e.currentTarget.releasePointerCapture(e.pointerId);
+              } catch (_) {
+                void null;
+              }
+            }}
+            onPointerMove={actions.onMove.moving}
+          />
+        </div>
+      ) : null}
+    </>
+  );
+
+  const offsetStyle: React.CSSProperties = {
+    transform:
+      arbitraryOffset != null
+        ? `translate(0,${arbitraryOffset}px)`
+        : offset != null
+        ? `translate(0,${offset * 100}%)`
+        : undefined,
+    transition: offset != null ? "transform 0.5s" : undefined,
+    zIndex: arbitraryOffset != null ? 1 : undefined,
+  };
+
+  return actions == null ? (
+    <Link
+      to={`${timeline.timelineOwner}/${timeline.timelineName}`}
+      className="timeline-board-item"
+    >
+      {content}
+    </Link>
+  ) : (
+    <div style={offsetStyle} className="timeline-board-item">
+      {content}
+    </div>
+  );
+};
+
+interface TimelineBoardItemContainerProps {
+  timelines: TimelineBookmark[];
+  editHandler?: {
+    // offset may exceed index range plusing index.
+    onMove: (
+      owner: string,
+      timeline: string,
+      index: number,
+      offset: number
+    ) => void;
+    onDelete: (owner: string, timeline: string) => void;
+  };
+}
+
+const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({
+  timelines,
+  editHandler,
+}) => {
+  const [moveState, setMoveState] = React.useState<null | {
+    index: number;
+    offset: number;
+    startPointY: number;
+  }>(null);
+
+  return (
+    <>
+      {timelines.map((timeline, index) => {
+        const height = 48;
+
+        let offset: number | undefined = undefined;
+        let arbitraryOffset: number | undefined = undefined;
+        if (moveState != null) {
+          if (index === moveState.index) {
+            arbitraryOffset = moveState.offset;
+          } else {
+            if (moveState.offset >= 0) {
+              const offsetCount = Math.round(moveState.offset / height);
+              if (
+                index > moveState.index &&
+                index <= moveState.index + offsetCount
+              ) {
+                offset = -1;
+              } else {
+                offset = 0;
+              }
+            } else {
+              const offsetCount = Math.round(-moveState.offset / height);
+              if (
+                index < moveState.index &&
+                index >= moveState.index - offsetCount
+              ) {
+                offset = 1;
+              } else {
+                offset = 0;
+              }
+            }
+          }
+        }
+
+        return (
+          <TimelineBoardItem
+            key={timeline.timelineOwner + "/" + timeline.timelineName}
+            timeline={timeline}
+            offset={offset}
+            arbitraryOffset={arbitraryOffset}
+            actions={
+              editHandler != null
+                ? {
+                    onDelete: () => {
+                      editHandler.onDelete(
+                        timeline.timelineOwner,
+                        timeline.timelineName
+                      );
+                    },
+                    onMove: {
+                      start: (e) => {
+                        if (moveState != null) return;
+                        setMoveState({
+                          index,
+                          offset: 0,
+                          startPointY: e.clientY,
+                        });
+                      },
+                      moving: (e) => {
+                        if (moveState == null) return;
+                        setMoveState({
+                          index,
+                          offset: e.clientY - moveState.startPointY,
+                          startPointY: moveState.startPointY,
+                        });
+                      },
+                      end: () => {
+                        if (moveState != null) {
+                          const offsetCount = Math.round(
+                            moveState.offset / height
+                          );
+                          editHandler.onMove(
+                            timeline.timelineOwner,
+                            timeline.timelineName,
+                            moveState.index,
+                            offsetCount
+                          );
+                        }
+                        setMoveState(null);
+                      },
+                    },
+                  }
+                : undefined
+            }
+          />
+        );
+      })}
+    </>
+  );
+};
+
+interface TimelineBoardUIProps {
+  title?: string | null;
+  state: "offline" | "loading" | "loaded";
+  timelines: TimelineBookmark[];
+  onReload: () => void;
+  className?: string;
+  editHandler?: {
+    onMove: (
+      owner: string,
+      timeline: string,
+      index: number,
+      offset: number
+    ) => void;
+    onDelete: (owner: string, timeline: string) => void;
+  };
+}
+
+const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
+  const { title, state, timelines, className, editHandler } = props;
+
+  const editable = editHandler != null;
+
+  const [editing, setEditing] = React.useState<boolean>(false);
+
+  return (
+    <Card className={classnames("timeline-board", className)}>
+      <div className="timeline-board-header">
+        {title != null && <h3>{title}</h3>}
+        {editable &&
+          (editing ? (
+            <FlatButton
+              text="done"
+              onClick={() => {
+                setEditing(false);
+              }}
+            />
+          ) : (
+            <FlatButton
+              text="edit"
+              onClick={() => {
+                setEditing(true);
+              }}
+            />
+          ))}
+      </div>
+      {(() => {
+        if (state === "loading") {
+          return (
+            <div className="d-flex flex-grow-1 justify-content-center align-items-center">
+              <Spinner />
+            </div>
+          );
+        } else if (state === "offline") {
+          return (
+            <div className="d-flex flex-grow-1 justify-content-center align-items-center">
+              <LoadFailReload onReload={props.onReload} />
+            </div>
+          );
+        } else {
+          return (
+            <TimelineBoardItemContainer
+              timelines={timelines}
+              editHandler={
+                editHandler && editing
+                  ? {
+                      onDelete: editHandler.onDelete,
+                      onMove: (owner, timeline, index, offset) => {
+                        if (index + offset >= timelines.length) {
+                          offset = timelines.length - index - 1;
+                        } else if (index + offset < 0) {
+                          offset = -index;
+                        }
+                        editHandler.onMove(owner, timeline, index, offset);
+                      },
+                    }
+                  : undefined
+              }
+            />
+          );
+        }
+      })()}
+    </Card>
+  );
+};
+
+export interface TimelineBoardProps {
+  title?: string | null;
+  className?: string;
+  load: () => Promise<TimelineBookmark[]>;
+  editHandler?: {
+    onMove: (
+      owner: string,
+      timeline: string,
+      index: number,
+      offset: number
+    ) => Promise<void>;
+    onDelete: (owner: string, timeline: string) => Promise<void>;
+  };
+}
+
+const TimelineBoard: React.FC<TimelineBoardProps> = ({
+  className,
+  title,
+  load,
+  editHandler,
+}) => {
+  const [state, setState] = React.useState<"offline" | "loading" | "loaded">(
+    "loading"
+  );
+  const [timelines, setTimelines] = React.useState<TimelineBookmark[]>([]);
+
+  React.useEffect(() => {
+    let subscribe = true;
+    if (state === "loading") {
+      void load().then(
+        (timelines) => {
+          if (subscribe) {
+            setState("loaded");
+            setTimelines(timelines);
+          }
+        },
+        () => {
+          setState("offline");
+        }
+      );
+    }
+    return () => {
+      subscribe = false;
+    };
+  }, [load, state]);
+
+  return (
+    <TimelineBoardUI
+      title={title}
+      className={className}
+      state={state}
+      timelines={timelines}
+      onReload={() => {
+        setState("loaded");
+      }}
+      editHandler={
+        typeof timelines === "object" && editHandler != null
+          ? {
+              onMove: (owner, timeline, index, offset) => {
+                const newTimelines = timelines.slice();
+                const [t] = newTimelines.splice(index, 1);
+                newTimelines.splice(index + offset, 0, t);
+                setTimelines(newTimelines);
+                editHandler
+                  .onMove(owner, timeline, index, offset)
+                  .then(null, () => {
+                    setTimelines(timelines);
+                  });
+              },
+              onDelete: (owner, timeline) => {
+                const newTimelines = timelines.slice();
+                newTimelines.splice(
+                  timelines.findIndex(
+                    (t) =>
+                      t.timelineOwner === owner && t.timelineName === timeline
+                  ),
+                  1
+                );
+                setTimelines(newTimelines);
+                editHandler.onDelete(owner, timeline).then(null, () => {
+                  setTimelines(timelines);
+                });
+              },
+            }
+          : undefined
+      }
+    />
+  );
+};
+
+export default TimelineBoard;
diff --git a/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx
new file mode 100644
index 00000000..63742936
--- /dev/null
+++ b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx
@@ -0,0 +1,57 @@
+import * as React from "react";
+import { useNavigate } from "react-router-dom";
+
+import { validateTimelineName } from "@/services/timeline";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
+
+import OperationDialog from "../common/dialog/OperationDialog";
+import { useUserLoggedIn } from "@/services/user";
+
+interface TimelineCreateDialogProps {
+  open: boolean;
+  close: () => void;
+}
+
+const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => {
+  const navigate = useNavigate();
+
+  const user = useUserLoggedIn();
+
+  return (
+    <OperationDialog
+      open={props.open}
+      onClose={props.close}
+      themeColor="success"
+      title="home.createDialog.title"
+      inputScheme={
+        [
+          {
+            type: "text",
+            label: "home.createDialog.name",
+            helperText: "home.createDialog.nameFormat",
+          },
+        ] as const
+      }
+      inputValidator={([name]) => {
+        if (name.length === 0) {
+          return { 0: "home.createDialog.noEmpty" };
+        } else if (name.length > 26) {
+          return { 0: "home.createDialog.tooLong" };
+        } else if (!validateTimelineName(name)) {
+          return { 0: "home.createDialog.badFormat" };
+        } else {
+          return null;
+        }
+      }}
+      onProcess={([name]): Promise<HttpTimelineInfo> =>
+        getHttpTimelineClient().postTimeline({ name })
+      }
+      onSuccessAndClose={(timeline: HttpTimelineInfo) => {
+        navigate(`${user.username}/${timeline.nameV2}`);
+      }}
+      failurePrompt={(e) => `${e as string}`}
+    />
+  );
+};
+
+export default TimelineCreateDialog;
diff --git a/FrontEnd/src/migrating/center/index.css b/FrontEnd/src/migrating/center/index.css
new file mode 100644
index 00000000..a779ff90
--- /dev/null
+++ b/FrontEnd/src/migrating/center/index.css
@@ -0,0 +1,43 @@
+.timeline-board {
+  min-height: 200px;
+  height: 100%;
+  position: relative;
+  padding: 1em 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.timeline-board-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 1em;
+}
+
+.timeline-board-item {
+  font-size: 1.1em;
+  height: 48px;
+  transition: background 0.3s;
+  display: flex;
+  align-items: center;
+  padding: 0 1em;
+}
+
+.timeline-board-item .icon {
+  height: 1.3em;
+  color: black;
+}
+
+.timeline-board-item:hover {
+  background: #dee2e6;
+}
+.timeline-board-item .right {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+.timeline-board-item .title {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
diff --git a/FrontEnd/src/migrating/center/index.tsx b/FrontEnd/src/migrating/center/index.tsx
new file mode 100644
index 00000000..77af2c20
--- /dev/null
+++ b/FrontEnd/src/migrating/center/index.tsx
@@ -0,0 +1,60 @@
+import * as React from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useUserLoggedIn } from "@/services/user";
+
+import SearchInput from "../common/SearchInput";
+import Button from "../common/button/Button";
+import CenterBoards from "./CenterBoards";
+import TimelineCreateDialog from "./TimelineCreateDialog";
+
+import "./index.css";
+
+const HomePage: React.FC = () => {
+  const navigate = useNavigate();
+
+  const user = useUserLoggedIn();
+
+  const [navText, setNavText] = React.useState<string>("");
+
+  const [dialog, setDialog] = React.useState<"create" | null>(null);
+
+  return (
+    <>
+      <div className="container">
+        <div className="row my-3 justify-content-center">
+          <div className="col col-12 col-md-8">
+            <SearchInput
+              className="justify-content-center"
+              value={navText}
+              onChange={setNavText}
+              onButtonClick={() => {
+                navigate(`search?q=${navText}`);
+              }}
+              additionalButton={
+                user != null && (
+                  <Button
+                    text="home.createButton"
+                    color="success"
+                    onClick={() => {
+                      setDialog("create");
+                    }}
+                  />
+                )
+              }
+            />
+          </div>
+        </div>
+        <CenterBoards />
+      </div>
+      <TimelineCreateDialog
+        open={dialog === "create"}
+        close={() => {
+          setDialog(null);
+        }}
+      />
+    </>
+  );
+};
+
+export default HomePage;
diff --git a/FrontEnd/src/services/alert.ts b/FrontEnd/src/services/alert.ts
index 42b14451..2f66dccc 100644
--- a/FrontEnd/src/services/alert.ts
+++ b/FrontEnd/src/services/alert.ts
@@ -1,10 +1,10 @@
 import pull from "lodash/pull";
 
 import { I18nText } from "@/common";
-import { PaletteColorType } from "@/palette";
+import { ThemeColor } from "@/views/common/common";
 
 export interface AlertInfo {
-  type?: PaletteColorType;
+  type?: ThemeColor;
   message?: I18nText;
   customMessage?: React.ReactElement;
   dismissTime?: number | "never";
diff --git a/FrontEnd/src/views/admin/Admin.tsx b/FrontEnd/src/views/admin/Admin.tsx
deleted file mode 100644
index 986c36b4..00000000
--- a/FrontEnd/src/views/admin/Admin.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Route, Routes } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-
-import AdminNav from "./AdminNav";
-import UserAdmin from "./UserAdmin";
-import MoreAdmin from "./MoreAdmin";
-
-import "./index.css";
-
-const Admin: React.FC = () => {
-  useTranslation("admin");
-
-  return (
-    <>
-      <div className="container">
-        <AdminNav className="mt-2" />
-        <Routes>
-          <Route index element={<UserAdmin />} />
-          <Route path="user" element={<UserAdmin />} />
-          <Route path="more" element={<MoreAdmin />} />
-        </Routes>
-      </div>
-    </>
-  );
-};
-
-export default Admin;
diff --git a/FrontEnd/src/views/admin/AdminNav.tsx b/FrontEnd/src/views/admin/AdminNav.tsx
deleted file mode 100644
index b7385e5c..00000000
--- a/FrontEnd/src/views/admin/AdminNav.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useLocation } from "react-router-dom";
-
-import Tabs from "../common/tab/Tabs";
-
-export function AdminNav({ className }: { className?: string }) {
-  const location = useLocation();
-  const name = location.pathname.split("/")[2] ?? "user";
-
-  return (
-    <Tabs
-      className={className}
-      activeTabName={name}
-      tabs={[
-        {
-          name: "user",
-          text: "admin:nav.users",
-          link: "/admin/user",
-        },
-        {
-          name: "more",
-          text: "admin:nav.more",
-          link: "/admin/more",
-        },
-      ]}
-    />
-  );
-}
-
-export default AdminNav;
diff --git a/FrontEnd/src/views/admin/MoreAdmin.tsx b/FrontEnd/src/views/admin/MoreAdmin.tsx
deleted file mode 100644
index d49d211f..00000000
--- a/FrontEnd/src/views/admin/MoreAdmin.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as React from "react";
-
-const MoreAdmin: React.FC = () => {
-  return <>More...</>;
-};
-
-export default MoreAdmin;
diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx
deleted file mode 100644
index d5179bf5..00000000
--- a/FrontEnd/src/views/admin/UserAdmin.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-import { useState, useEffect } from "react";
-import * as React from "react";
-import { Trans, useTranslation } from "react-i18next";
-import classnames from "classnames";
-
-import { getHttpUserClient, HttpUser, kUserPermissionList } from "@/http/user";
-
-import OperationDialog, {
-  OperationDialogBoolInput,
-} from "../common/dialog/OperationDialog";
-import Button from "../common/button/Button";
-import Spinner from "../common/Spinner";
-import FlatButton from "../common/button/FlatButton";
-import IconButton from "../common/button/IconButton";
-
-const CreateUserDialog: React.FC<{
-  open: boolean;
-  close: () => void;
-  onSuccess: (user: HttpUser) => void;
-}> = ({ open, close, onSuccess }) => {
-  return (
-    <OperationDialog
-      title="admin:user.dialog.create.title"
-      themeColor="success"
-      inputPrompt="admin:user.dialog.create.prompt"
-      inputScheme={
-        [
-          { type: "text", label: "admin:user.username" },
-          { type: "text", label: "admin:user.password" },
-        ] as const
-      }
-      onProcess={([username, password]) =>
-        getHttpUserClient().post({
-          username,
-          password,
-        })
-      }
-      onClose={close}
-      open={open}
-      onSuccessAndClose={onSuccess}
-    />
-  );
-};
-
-const UsernameLabel: React.FC<{ children: React.ReactNode }> = (props) => {
-  return <span style={{ color: "blue" }}>{props.children}</span>;
-};
-
-const UserDeleteDialog: React.FC<{
-  open: boolean;
-  close: () => void;
-  user: HttpUser;
-  onSuccess: () => void;
-}> = ({ open, close, user, onSuccess }) => {
-  return (
-    <OperationDialog
-      open={open}
-      onClose={close}
-      title="admin:user.dialog.delete.title"
-      themeColor="danger"
-      inputPrompt={() => (
-        <Trans i18nKey="user.dialog.delete.prompt" ns="admin">
-          0<UsernameLabel>{user.username}</UsernameLabel>2
-        </Trans>
-      )}
-      onProcess={() => getHttpUserClient().delete(user.username)}
-      onSuccessAndClose={onSuccess}
-    />
-  );
-};
-
-const UserModifyDialog: React.FC<{
-  open: boolean;
-  close: () => void;
-  user: HttpUser;
-  onSuccess: () => void;
-}> = ({ open, close, user, onSuccess }) => {
-  return (
-    <OperationDialog
-      open={open}
-      onClose={close}
-      title="admin:user.dialog.modify.title"
-      themeColor="danger"
-      inputPrompt={() => (
-        <Trans i18nKey="admin:user.dialog.modify.prompt">
-          0<UsernameLabel>{user.username}</UsernameLabel>2
-        </Trans>
-      )}
-      inputScheme={
-        [
-          {
-            type: "text",
-            label: "admin:user.username",
-            initValue: user.username,
-          },
-          { type: "text", label: "admin:user.password" },
-          {
-            type: "text",
-            label: "admin:user.nickname",
-            initValue: user.nickname,
-          },
-        ] as const
-      }
-      onProcess={([username, password, nickname]) =>
-        getHttpUserClient().patch(user.username, {
-          username: username !== user.username ? username : undefined,
-          password: password !== "" ? password : undefined,
-          nickname: nickname !== user.nickname ? nickname : undefined,
-        })
-      }
-      onSuccessAndClose={onSuccess}
-    />
-  );
-};
-
-const UserPermissionModifyDialog: React.FC<{
-  open: boolean;
-  close: () => void;
-  user: HttpUser;
-  onSuccess: () => void;
-}> = ({ open, close, user, onSuccess }) => {
-  const oldPermissionBoolList: boolean[] = kUserPermissionList.map(
-    (permission) => user.permissions.includes(permission)
-  );
-
-  return (
-    <OperationDialog
-      open={open}
-      onClose={close}
-      title="admin:user.dialog.modifyPermissions.title"
-      themeColor="danger"
-      inputPrompt={() => (
-        <Trans i18nKey="admin:user.dialog.modifyPermissions.prompt">
-          0<UsernameLabel>{user.username}</UsernameLabel>2
-        </Trans>
-      )}
-      inputScheme={kUserPermissionList.map<OperationDialogBoolInput>(
-        (permission, index) => ({
-          type: "bool",
-          label: { type: "custom", value: permission },
-          initValue: oldPermissionBoolList[index],
-        })
-      )}
-      onProcess={async (newPermissionBoolList): Promise<boolean[]> => {
-        for (let index = 0; index < kUserPermissionList.length; index++) {
-          const oldValue = oldPermissionBoolList[index];
-          const newValue = newPermissionBoolList[index];
-          const permission = kUserPermissionList[index];
-          if (oldValue === newValue) continue;
-          if (newValue) {
-            await getHttpUserClient().putUserPermission(
-              user.username,
-              permission
-            );
-          } else {
-            await getHttpUserClient().deleteUserPermission(
-              user.username,
-              permission
-            );
-          }
-        }
-        return newPermissionBoolList;
-      }}
-      onSuccessAndClose={onSuccess}
-    />
-  );
-};
-
-interface UserItemProps {
-  user: HttpUser;
-  onChange: () => void;
-}
-
-const UserItem: React.FC<UserItemProps> = ({ user, onChange }) => {
-  const { t } = useTranslation();
-
-  const [dialog, setDialog] = useState<
-    "delete" | "modify" | "permission" | null
-  >(null);
-
-  const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false);
-
-  return (
-    <>
-      <div className="admin-user-item">
-        <IconButton
-          icon="pencil-square"
-          className="cru-float-right"
-          onClick={() => setEditMaskVisible(true)}
-        />
-        <h5 className="cru-color-primary">{user.username}</h5>
-        <small className="d-block cru-color-secondary">
-          {t("admin:user.nickname")}
-          {user.nickname}
-        </small>
-        <small className="d-block cru-color-secondary">
-          {t("admin:user.uniqueId")}
-          {user.uniqueId}
-        </small>
-        <small className="d-block cru-color-secondary">
-          {t("admin:user.permissions")}
-          {user.permissions.map((permission) => {
-            return (
-              <span key={permission} className="cru-color-danger">
-                {permission}
-              </span>
-            );
-          })}
-        </small>
-        <div
-          className={classnames("edit-mask", !editMaskVisible && "d-none")}
-          onClick={() => setEditMaskVisible(false)}
-        >
-          <FlatButton
-            text="admin:user.modify"
-            onClick={() => setDialog("modify")}
-          />
-          <FlatButton
-            text="admin:user.modifyPermissions"
-            onClick={() => setDialog("permission")}
-          />
-          <FlatButton
-            text="admin:user.delete"
-            color="danger"
-            onClick={() => setDialog("delete")}
-          />
-        </div>
-      </div>
-      <UserDeleteDialog
-        open={dialog === "delete"}
-        close={() => setDialog(null)}
-        user={user}
-        onSuccess={onChange}
-      />
-      <UserModifyDialog
-        open={dialog === "modify"}
-        close={() => setDialog(null)}
-        user={user}
-        onSuccess={onChange}
-      />
-      <UserPermissionModifyDialog
-        open={dialog === "permission"}
-        close={() => setDialog(null)}
-        user={user}
-        onSuccess={onChange}
-      />
-    </>
-  );
-};
-
-const UserAdmin: React.FC = () => {
-  const [users, setUsers] = useState<HttpUser[] | null>(null);
-  const [dialog, setDialog] = useState<"create" | null>(null);
-  const [usersVersion, setUsersVersion] = useState<number>(0);
-  const updateUsers = (): void => {
-    setUsersVersion(usersVersion + 1);
-  };
-
-  useEffect(() => {
-    let subscribe = true;
-    void getHttpUserClient()
-      .list()
-      .then((us) => {
-        if (subscribe) {
-          setUsers(us.items);
-        }
-      });
-    return () => {
-      subscribe = false;
-    };
-  }, [usersVersion]);
-
-  if (users) {
-    const userComponents = users.map((user) => {
-      return (
-        <UserItem key={user.username} user={user} onChange={updateUsers} />
-      );
-    });
-
-    return (
-      <>
-        <div className="row justify-content-end my-2">
-          <div className="col col-auto">
-            <Button
-              text="admin:create"
-              color="success"
-              onClick={() => setDialog("create")}
-            />
-          </div>
-        </div>
-        {userComponents}
-        <CreateUserDialog
-          open={dialog === "create"}
-          close={() => setDialog(null)}
-          onSuccess={updateUsers}
-        />
-      </>
-    );
-  } else {
-    return <Spinner />;
-  }
-};
-
-export default UserAdmin;
diff --git a/FrontEnd/src/views/admin/index.css b/FrontEnd/src/views/admin/index.css
deleted file mode 100644
index 17e24586..00000000
--- a/FrontEnd/src/views/admin/index.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.admin-user-item {
-  position: relative;
-  border: var(--cru-primary-color) solid;
-  border-width: 1px 1px 0;
-  padding: 1em;
-}
-
-.admin-user-item:last-of-type {
-  border-bottom-width: 1px;
-}
-
-.admin-user-item .edit-mask {
-  position: absolute;
-  top: 0;
-  left: 0;
-  bottom: 0;
-  right: 0;
-  background: rgba(255, 255, 255, 0.9);
-  position: absolute;
-  display: flex;
-  justify-content: space-around;
-  align-items: center;
-}
-
-@media (max-width: 576px) {
-  .admin-user-item .edit-mask {
-    flex-direction: column;
-  }
-}
-
-.admin-user-item .edit-mask button {
-  margin: 0.5em 2em;
-}
diff --git a/FrontEnd/src/views/admin/index.tsx b/FrontEnd/src/views/admin/index.tsx
deleted file mode 100644
index 0467711d..00000000
--- a/FrontEnd/src/views/admin/index.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { lazy } from "react";
-
-const Admin = lazy(
-  () => import(/* webpackChunkName: "admin" */ "./Admin")
-);
-
-export default Admin;
diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx
deleted file mode 100644
index a8be2c29..00000000
--- a/FrontEnd/src/views/center/CenterBoards.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-
-import { highlightTimelineUsername } from "@/common";
-
-import { pushAlert } from "@/services/alert";
-import { useUserLoggedIn } from "@/services/user";
-
-import { getHttpTimelineClient } from "@/http/timeline";
-import { getHttpBookmarkClient } from "@/http/bookmark";
-
-import TimelineBoard from "./TimelineBoard";
-
-const CenterBoards: React.FC = () => {
-  const { t } = useTranslation();
-
-  const user = useUserLoggedIn();
-
-  return (
-    <>
-      <div className="row justify-content-center">
-        <div className="col col-12 col-md-6">
-          <div className="row">
-            <div className="col col-12 my-2">
-              <TimelineBoard
-                title={t("home.bookmarkTimeline")}
-                load={() =>
-                  getHttpBookmarkClient()
-                    .list(user.username)
-                    .then((p) => p.items)
-                }
-                editHandler={{
-                  onDelete: (owner, timeline) => {
-                    return getHttpBookmarkClient()
-                      .delete(user.username, owner, timeline)
-                      .catch((e) => {
-                        pushAlert({
-                          message: "home.message.deleteBookmarkFail",
-                          type: "danger",
-                        });
-                        throw e;
-                      });
-                  },
-                  onMove: (owner, timeline, index, offset) => {
-                    return getHttpBookmarkClient()
-                      .move(
-                        user.username,
-                        owner,
-                        timeline,
-                        index + offset + 1 // +1 because backend contract: index starts at 1
-                      )
-                      .catch((e) => {
-                        pushAlert({
-                          message: "home.message.moveBookmarkFail",
-                          type: "danger",
-                        });
-                        throw e;
-                      })
-                      .then();
-                  },
-                }}
-              />
-            </div>
-            <div className="col col-12 my-2">
-              <TimelineBoard
-                title={t("home.highlightTimeline")}
-                load={() =>
-                  getHttpBookmarkClient()
-                    .list(highlightTimelineUsername)
-                    .then((p) => p.items)
-                }
-                editHandler={
-                  user.username === highlightTimelineUsername
-                    ? {
-                        onDelete: (owner, timeline) => {
-                          return getHttpBookmarkClient()
-                            .delete(highlightTimelineUsername, owner, timeline)
-                            .catch((e) => {
-                              pushAlert({
-                                message: "home.message.deleteHighlightFail",
-                                type: "danger",
-                              });
-                              throw e;
-                            });
-                        },
-                        onMove: (owner, timeline, index, offset) => {
-                          return getHttpBookmarkClient()
-                            .move(
-                              highlightTimelineUsername,
-                              owner,
-                              timeline,
-                              index + offset + 1 // +1 because backend contract: index starts at 1
-                            )
-                            .catch((e) => {
-                              pushAlert({
-                                message: "home.message.moveBookmarkFail",
-                                type: "danger",
-                              });
-                              throw e;
-                            })
-                            .then();
-                        },
-                      }
-                    : undefined
-                }
-              />
-            </div>
-          </div>
-        </div>
-        <div className="col-12 col-md-6 my-2">
-          <TimelineBoard
-            title={t("home.relatedTimeline")}
-            load={() =>
-              getHttpTimelineClient()
-                .listTimeline({ relate: user.username })
-                .then((l) =>
-                  l.map((t, index) => ({
-                    timelineOwner: t.owner.username,
-                    timelineName: t.nameV2,
-                    position: index + 1,
-                  }))
-                )
-            }
-          />
-        </div>
-      </div>
-    </>
-  );
-};
-
-export default CenterBoards;
diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx
deleted file mode 100644
index b3ccdf8c..00000000
--- a/FrontEnd/src/views/center/TimelineBoard.tsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { Link } from "react-router-dom";
-
-import { TimelineBookmark } from "@/http/bookmark";
-
-import TimelineLogo from "../common/TimelineLogo";
-import LoadFailReload from "../common/LoadFailReload";
-import FlatButton from "../common/button/FlatButton";
-import Card from "../common/Card";
-import Spinner from "../common/Spinner";
-import IconButton from "../common/button/IconButton";
-
-interface TimelineBoardItemProps {
-  timeline: TimelineBookmark;
-  // In height.
-  offset?: number;
-  // In px.
-  arbitraryOffset?: number;
-  // If not null, will disable navigation on click.
-  actions?: {
-    onDelete: () => void;
-    onMove: {
-      start: (e: React.PointerEvent) => void;
-      moving: (e: React.PointerEvent) => void;
-      end: (e: React.PointerEvent) => void;
-    };
-  };
-}
-
-const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({
-  timeline,
-  arbitraryOffset,
-  offset,
-  actions,
-}) => {
-  const content = (
-    <>
-      <TimelineLogo className="icon" />
-      <span className="title">
-        {timeline.timelineOwner}/{timeline.timelineName}
-      </span>
-      <span className="flex-grow-1"></span>
-      {actions != null ? (
-        <div className="right">
-          <IconButton
-            icon="trash"
-            color="danger"
-            className="px-2"
-            onClick={actions.onDelete}
-          />
-          <IconButton
-            icon="grip-vertical"
-            className="px-2 touch-action-none"
-            onPointerDown={(e) => {
-              e.currentTarget.setPointerCapture(e.pointerId);
-              actions.onMove.start(e);
-            }}
-            onPointerUp={(e) => {
-              actions.onMove.end(e);
-              try {
-                e.currentTarget.releasePointerCapture(e.pointerId);
-              } catch (_) {
-                void null;
-              }
-            }}
-            onPointerMove={actions.onMove.moving}
-          />
-        </div>
-      ) : null}
-    </>
-  );
-
-  const offsetStyle: React.CSSProperties = {
-    transform:
-      arbitraryOffset != null
-        ? `translate(0,${arbitraryOffset}px)`
-        : offset != null
-        ? `translate(0,${offset * 100}%)`
-        : undefined,
-    transition: offset != null ? "transform 0.5s" : undefined,
-    zIndex: arbitraryOffset != null ? 1 : undefined,
-  };
-
-  return actions == null ? (
-    <Link
-      to={`${timeline.timelineOwner}/${timeline.timelineName}`}
-      className="timeline-board-item"
-    >
-      {content}
-    </Link>
-  ) : (
-    <div style={offsetStyle} className="timeline-board-item">
-      {content}
-    </div>
-  );
-};
-
-interface TimelineBoardItemContainerProps {
-  timelines: TimelineBookmark[];
-  editHandler?: {
-    // offset may exceed index range plusing index.
-    onMove: (
-      owner: string,
-      timeline: string,
-      index: number,
-      offset: number
-    ) => void;
-    onDelete: (owner: string, timeline: string) => void;
-  };
-}
-
-const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({
-  timelines,
-  editHandler,
-}) => {
-  const [moveState, setMoveState] = React.useState<null | {
-    index: number;
-    offset: number;
-    startPointY: number;
-  }>(null);
-
-  return (
-    <>
-      {timelines.map((timeline, index) => {
-        const height = 48;
-
-        let offset: number | undefined = undefined;
-        let arbitraryOffset: number | undefined = undefined;
-        if (moveState != null) {
-          if (index === moveState.index) {
-            arbitraryOffset = moveState.offset;
-          } else {
-            if (moveState.offset >= 0) {
-              const offsetCount = Math.round(moveState.offset / height);
-              if (
-                index > moveState.index &&
-                index <= moveState.index + offsetCount
-              ) {
-                offset = -1;
-              } else {
-                offset = 0;
-              }
-            } else {
-              const offsetCount = Math.round(-moveState.offset / height);
-              if (
-                index < moveState.index &&
-                index >= moveState.index - offsetCount
-              ) {
-                offset = 1;
-              } else {
-                offset = 0;
-              }
-            }
-          }
-        }
-
-        return (
-          <TimelineBoardItem
-            key={timeline.timelineOwner + "/" + timeline.timelineName}
-            timeline={timeline}
-            offset={offset}
-            arbitraryOffset={arbitraryOffset}
-            actions={
-              editHandler != null
-                ? {
-                    onDelete: () => {
-                      editHandler.onDelete(
-                        timeline.timelineOwner,
-                        timeline.timelineName
-                      );
-                    },
-                    onMove: {
-                      start: (e) => {
-                        if (moveState != null) return;
-                        setMoveState({
-                          index,
-                          offset: 0,
-                          startPointY: e.clientY,
-                        });
-                      },
-                      moving: (e) => {
-                        if (moveState == null) return;
-                        setMoveState({
-                          index,
-                          offset: e.clientY - moveState.startPointY,
-                          startPointY: moveState.startPointY,
-                        });
-                      },
-                      end: () => {
-                        if (moveState != null) {
-                          const offsetCount = Math.round(
-                            moveState.offset / height
-                          );
-                          editHandler.onMove(
-                            timeline.timelineOwner,
-                            timeline.timelineName,
-                            moveState.index,
-                            offsetCount
-                          );
-                        }
-                        setMoveState(null);
-                      },
-                    },
-                  }
-                : undefined
-            }
-          />
-        );
-      })}
-    </>
-  );
-};
-
-interface TimelineBoardUIProps {
-  title?: string | null;
-  state: "offline" | "loading" | "loaded";
-  timelines: TimelineBookmark[];
-  onReload: () => void;
-  className?: string;
-  editHandler?: {
-    onMove: (
-      owner: string,
-      timeline: string,
-      index: number,
-      offset: number
-    ) => void;
-    onDelete: (owner: string, timeline: string) => void;
-  };
-}
-
-const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
-  const { title, state, timelines, className, editHandler } = props;
-
-  const editable = editHandler != null;
-
-  const [editing, setEditing] = React.useState<boolean>(false);
-
-  return (
-    <Card className={classnames("timeline-board", className)}>
-      <div className="timeline-board-header">
-        {title != null && <h3>{title}</h3>}
-        {editable &&
-          (editing ? (
-            <FlatButton
-              text="done"
-              onClick={() => {
-                setEditing(false);
-              }}
-            />
-          ) : (
-            <FlatButton
-              text="edit"
-              onClick={() => {
-                setEditing(true);
-              }}
-            />
-          ))}
-      </div>
-      {(() => {
-        if (state === "loading") {
-          return (
-            <div className="d-flex flex-grow-1 justify-content-center align-items-center">
-              <Spinner />
-            </div>
-          );
-        } else if (state === "offline") {
-          return (
-            <div className="d-flex flex-grow-1 justify-content-center align-items-center">
-              <LoadFailReload onReload={props.onReload} />
-            </div>
-          );
-        } else {
-          return (
-            <TimelineBoardItemContainer
-              timelines={timelines}
-              editHandler={
-                editHandler && editing
-                  ? {
-                      onDelete: editHandler.onDelete,
-                      onMove: (owner, timeline, index, offset) => {
-                        if (index + offset >= timelines.length) {
-                          offset = timelines.length - index - 1;
-                        } else if (index + offset < 0) {
-                          offset = -index;
-                        }
-                        editHandler.onMove(owner, timeline, index, offset);
-                      },
-                    }
-                  : undefined
-              }
-            />
-          );
-        }
-      })()}
-    </Card>
-  );
-};
-
-export interface TimelineBoardProps {
-  title?: string | null;
-  className?: string;
-  load: () => Promise<TimelineBookmark[]>;
-  editHandler?: {
-    onMove: (
-      owner: string,
-      timeline: string,
-      index: number,
-      offset: number
-    ) => Promise<void>;
-    onDelete: (owner: string, timeline: string) => Promise<void>;
-  };
-}
-
-const TimelineBoard: React.FC<TimelineBoardProps> = ({
-  className,
-  title,
-  load,
-  editHandler,
-}) => {
-  const [state, setState] = React.useState<"offline" | "loading" | "loaded">(
-    "loading"
-  );
-  const [timelines, setTimelines] = React.useState<TimelineBookmark[]>([]);
-
-  React.useEffect(() => {
-    let subscribe = true;
-    if (state === "loading") {
-      void load().then(
-        (timelines) => {
-          if (subscribe) {
-            setState("loaded");
-            setTimelines(timelines);
-          }
-        },
-        () => {
-          setState("offline");
-        }
-      );
-    }
-    return () => {
-      subscribe = false;
-    };
-  }, [load, state]);
-
-  return (
-    <TimelineBoardUI
-      title={title}
-      className={className}
-      state={state}
-      timelines={timelines}
-      onReload={() => {
-        setState("loaded");
-      }}
-      editHandler={
-        typeof timelines === "object" && editHandler != null
-          ? {
-              onMove: (owner, timeline, index, offset) => {
-                const newTimelines = timelines.slice();
-                const [t] = newTimelines.splice(index, 1);
-                newTimelines.splice(index + offset, 0, t);
-                setTimelines(newTimelines);
-                editHandler
-                  .onMove(owner, timeline, index, offset)
-                  .then(null, () => {
-                    setTimelines(timelines);
-                  });
-              },
-              onDelete: (owner, timeline) => {
-                const newTimelines = timelines.slice();
-                newTimelines.splice(
-                  timelines.findIndex(
-                    (t) =>
-                      t.timelineOwner === owner && t.timelineName === timeline
-                  ),
-                  1
-                );
-                setTimelines(newTimelines);
-                editHandler.onDelete(owner, timeline).then(null, () => {
-                  setTimelines(timelines);
-                });
-              },
-            }
-          : undefined
-      }
-    />
-  );
-};
-
-export default TimelineBoard;
diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx
deleted file mode 100644
index 63742936..00000000
--- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import * as React from "react";
-import { useNavigate } from "react-router-dom";
-
-import { validateTimelineName } from "@/services/timeline";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
-
-import OperationDialog from "../common/dialog/OperationDialog";
-import { useUserLoggedIn } from "@/services/user";
-
-interface TimelineCreateDialogProps {
-  open: boolean;
-  close: () => void;
-}
-
-const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => {
-  const navigate = useNavigate();
-
-  const user = useUserLoggedIn();
-
-  return (
-    <OperationDialog
-      open={props.open}
-      onClose={props.close}
-      themeColor="success"
-      title="home.createDialog.title"
-      inputScheme={
-        [
-          {
-            type: "text",
-            label: "home.createDialog.name",
-            helperText: "home.createDialog.nameFormat",
-          },
-        ] as const
-      }
-      inputValidator={([name]) => {
-        if (name.length === 0) {
-          return { 0: "home.createDialog.noEmpty" };
-        } else if (name.length > 26) {
-          return { 0: "home.createDialog.tooLong" };
-        } else if (!validateTimelineName(name)) {
-          return { 0: "home.createDialog.badFormat" };
-        } else {
-          return null;
-        }
-      }}
-      onProcess={([name]): Promise<HttpTimelineInfo> =>
-        getHttpTimelineClient().postTimeline({ name })
-      }
-      onSuccessAndClose={(timeline: HttpTimelineInfo) => {
-        navigate(`${user.username}/${timeline.nameV2}`);
-      }}
-      failurePrompt={(e) => `${e as string}`}
-    />
-  );
-};
-
-export default TimelineCreateDialog;
diff --git a/FrontEnd/src/views/center/index.css b/FrontEnd/src/views/center/index.css
deleted file mode 100644
index a779ff90..00000000
--- a/FrontEnd/src/views/center/index.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.timeline-board {
-  min-height: 200px;
-  height: 100%;
-  position: relative;
-  padding: 1em 0;
-  display: flex;
-  flex-direction: column;
-}
-
-.timeline-board-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 1em;
-}
-
-.timeline-board-item {
-  font-size: 1.1em;
-  height: 48px;
-  transition: background 0.3s;
-  display: flex;
-  align-items: center;
-  padding: 0 1em;
-}
-
-.timeline-board-item .icon {
-  height: 1.3em;
-  color: black;
-}
-
-.timeline-board-item:hover {
-  background: #dee2e6;
-}
-.timeline-board-item .right {
-  display: flex;
-  align-items: center;
-  flex-shrink: 0;
-}
-.timeline-board-item .title {
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
diff --git a/FrontEnd/src/views/center/index.tsx b/FrontEnd/src/views/center/index.tsx
deleted file mode 100644
index 77af2c20..00000000
--- a/FrontEnd/src/views/center/index.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import * as React from "react";
-import { useNavigate } from "react-router-dom";
-
-import { useUserLoggedIn } from "@/services/user";
-
-import SearchInput from "../common/SearchInput";
-import Button from "../common/button/Button";
-import CenterBoards from "./CenterBoards";
-import TimelineCreateDialog from "./TimelineCreateDialog";
-
-import "./index.css";
-
-const HomePage: React.FC = () => {
-  const navigate = useNavigate();
-
-  const user = useUserLoggedIn();
-
-  const [navText, setNavText] = React.useState<string>("");
-
-  const [dialog, setDialog] = React.useState<"create" | null>(null);
-
-  return (
-    <>
-      <div className="container">
-        <div className="row my-3 justify-content-center">
-          <div className="col col-12 col-md-8">
-            <SearchInput
-              className="justify-content-center"
-              value={navText}
-              onChange={setNavText}
-              onButtonClick={() => {
-                navigate(`search?q=${navText}`);
-              }}
-              additionalButton={
-                user != null && (
-                  <Button
-                    text="home.createButton"
-                    color="success"
-                    onClick={() => {
-                      setDialog("create");
-                    }}
-                  />
-                )
-              }
-            />
-          </div>
-        </div>
-        <CenterBoards />
-      </div>
-      <TimelineCreateDialog
-        open={dialog === "create"}
-        close={() => {
-          setDialog(null);
-        }}
-      />
-    </>
-  );
-};
-
-export default HomePage;
diff --git a/FrontEnd/src/views/common/button/LoadingButton.css b/FrontEnd/src/views/common/button/LoadingButton.css
index 2f73116a..0a7e4a3a 100644
--- a/FrontEnd/src/views/common/button/LoadingButton.css
+++ b/FrontEnd/src/views/common/button/LoadingButton.css
@@ -11,4 +11,4 @@
 
 .cru-loading-button-spinner {
   margin-left: 0.5em;
-}
+}
\ No newline at end of file
diff --git a/FrontEnd/src/views/search/index.css b/FrontEnd/src/views/search/index.css
deleted file mode 100644
index 6ff4d9fa..00000000
--- a/FrontEnd/src/views/search/index.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.timeline-search-result-item {
-  border: 1px solid;
-  border-color: #e9ecef;
-  background: #f8f9fa;
-  transition: all 0.3s;
-}
-.timeline-search-result-item:hover {
-  border-color: #0d6efd;
-}
-
-.timeline-search-result-item-avatar {
-  width: 2em;
-  height: 2em;
-  border-radius: 50%;
-}
diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx
deleted file mode 100644
index 58257465..00000000
--- a/FrontEnd/src/views/search/index.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-import { useNavigate, useLocation } from "react-router-dom";
-import { Link } from "react-router-dom";
-
-import { HttpNetworkError } from "@/http/common";
-import { getHttpSearchClient } from "@/http/search";
-import { HttpTimelineInfo } from "@/http/timeline";
-
-import SearchInput from "../common/SearchInput";
-import UserAvatar from "../common/user/UserAvatar";
-
-import "./index.css";
-
-const TimelineSearchResultItemView: React.FC<{
-  timeline: HttpTimelineInfo;
-}> = ({ timeline }) => {
-  return (
-    <div className="timeline-search-result-item my-2 p-3">
-      <h4>
-        <Link
-          to={`/${timeline.owner.username}/${timeline.nameV2}`}
-          className="mb-2 text-primary"
-        >
-          {timeline.title}
-          <small className="ms-3 text-secondary">{timeline.nameV2}</small>
-        </Link>
-      </h4>
-      <div>
-        <UserAvatar
-          username={timeline.owner.username}
-          className="timeline-search-result-item-avatar me-2"
-        />
-        {timeline.owner.nickname}
-        <small className="ms-3 text-secondary">
-          @{timeline.owner.username}
-        </small>
-      </div>
-    </div>
-  );
-};
-
-const SearchPage: React.FC = () => {
-  const { t } = useTranslation();
-
-  const navigate = useNavigate();
-  const location = useLocation();
-  const searchParams = new URLSearchParams(location.search);
-  const queryParam = searchParams.get("q");
-
-  const [searchText, setSearchText] = React.useState<string>("");
-  const [state, setState] = React.useState<
-    HttpTimelineInfo[] | "init" | "loading" | "network-error" | "error"
-  >("init");
-
-  const [forceResearchKey, setForceResearchKey] = React.useState<number>(0);
-
-  React.useEffect(() => {
-    setState("init");
-    if (queryParam != null && queryParam.length > 0) {
-      setSearchText(queryParam);
-      setState("loading");
-      void getHttpSearchClient()
-        .searchTimelines(queryParam)
-        .then(
-          (ts) => {
-            setState(ts);
-          },
-          (e) => {
-            if (e instanceof HttpNetworkError) {
-              setState("network-error");
-            } else {
-              setState("error");
-            }
-          }
-        );
-    }
-  }, [queryParam, forceResearchKey]);
-
-  return (
-    <div className="container my-3">
-      <div className="row justify-content-center">
-        <SearchInput
-          className="col-12 col-sm-9 col-md-6"
-          value={searchText}
-          onChange={setSearchText}
-          loading={state === "loading"}
-          onButtonClick={() => {
-            if (queryParam === searchText) {
-              setForceResearchKey((old) => old + 1);
-            } else {
-              navigate(`/search?q=${searchText}`);
-            }
-          }}
-        />
-      </div>
-      {(() => {
-        switch (state) {
-          case "init": {
-            if (queryParam == null || queryParam.length === 0) {
-              return <div>{t("searchPage.input")}</div>;
-            }
-            break;
-          }
-          case "loading": {
-            return <div>{t("searchPage.loading")}</div>;
-          }
-          case "network-error": {
-            return <div className="text-danger">{t("error.network")}</div>;
-          }
-          case "error": {
-            return <div className="text-danger">{t("error.unknown")}</div>;
-          }
-          default: {
-            if (state.length === 0) {
-              return <div>{t("searchPage.noResult")}</div>;
-            }
-            return state.map((t) => (
-              <TimelineSearchResultItemView
-                key={`${t.owner.username}/${t.nameV2}`}
-                timeline={t}
-              />
-            ));
-          }
-        }
-      })()}
-    </div>
-  );
-};
-
-export default SearchPage;
diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json
index 97946126..cd6aa25d 100644
--- a/FrontEnd/tsconfig.json
+++ b/FrontEnd/tsconfig.json
@@ -29,5 +29,8 @@
   },
   "include": [
     "src"
+  ],
+  "exclude": [
+    "src/migrating"
   ]
 }
\ No newline at end of file
-- 
cgit v1.2.3

