From 47587812b809fee2a95c76266d9d0e42fc4ac1ca Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Tue, 15 Jun 2021 14:14:28 +0800
Subject: ...

---
 .../views/timeline-common/TimelinePostListView.tsx | 79 ++++++++++++++++++++++
 1 file changed, 79 insertions(+)
 create mode 100644 FrontEnd/src/views/timeline-common/TimelinePostListView.tsx

(limited to 'FrontEnd/src/views/timeline-common/TimelinePostListView.tsx')

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


From e0b766203d7576ab67b16ba556ba14120d0bc876 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Tue, 15 Jun 2021 15:02:49 +0800
Subject: ...

---
 BackEnd/Timeline/FrontEndMode.cs                   |  1 -
 BackEnd/Timeline/Properties/launchSettings.json    |  2 +-
 BackEnd/Timeline/Startup.cs                        |  4 ---
 FrontEnd/index.html                                | 29 ++++++++++++++++++++++
 FrontEnd/package.json                              |  5 ++--
 FrontEnd/postcss.config.js                         | 11 +++-----
 FrontEnd/src/index.ejs                             | 29 ----------------------
 FrontEnd/src/services/TimelinePostBuilder.ts       |  4 +--
 FrontEnd/src/services/timeline.ts                  |  4 +--
 FrontEnd/src/services/user.ts                      |  6 ++---
 FrontEnd/src/tsconfig.json                         | 23 -----------------
 FrontEnd/src/views/admin/UserAdmin.tsx             |  2 +-
 FrontEnd/src/views/center/CenterBoards.tsx         |  6 ++---
 FrontEnd/src/views/center/TimelineBoard.tsx        |  4 +--
 FrontEnd/src/views/center/TimelineCreateDialog.tsx |  2 +-
 FrontEnd/src/views/common/user/UserAvatar.tsx      |  2 +-
 FrontEnd/src/views/home/TimelineListView.tsx       |  4 +--
 FrontEnd/src/views/home/index.tsx                  |  4 +--
 FrontEnd/src/views/search/index.tsx                | 10 ++++----
 FrontEnd/src/views/settings/ChangeAvatarDialog.tsx |  2 +-
 .../src/views/settings/ChangeNicknameDialog.tsx    |  2 +-
 .../timeline-common/ConnectionStatusBadge.tsx      |  2 +-
 .../src/views/timeline-common/MarkdownPostEdit.tsx |  2 +-
 .../timeline-common/PostPropertyChangeDialog.tsx   |  2 +-
 FrontEnd/src/views/timeline-common/Timeline.tsx    |  6 ++---
 .../src/views/timeline-common/TimelineMember.tsx   |  8 +++---
 .../timeline-common/TimelinePageCardTemplate.tsx   |  4 +--
 .../views/timeline-common/TimelinePageTemplate.tsx |  4 +--
 .../timeline-common/TimelinePagedPostListView.tsx  |  2 +-
 .../timeline-common/TimelinePostContentView.tsx    |  4 +--
 .../src/views/timeline-common/TimelinePostEdit.tsx |  6 ++---
 .../views/timeline-common/TimelinePostListView.tsx |  2 +-
 .../src/views/timeline-common/TimelinePostView.tsx |  2 +-
 .../TimelinePropertyChangeDialog.tsx               |  2 +-
 FrontEnd/src/views/timeline/TimelineCard.tsx       |  2 +-
 .../src/views/timeline/TimelineDeleteDialog.tsx    |  2 +-
 FrontEnd/src/views/user/index.tsx                  |  2 +-
 FrontEnd/tsconfig.json                             | 25 +++++++++++++++++++
 FrontEnd/vite.config.js                            |  3 +++
 39 files changed, 116 insertions(+), 120 deletions(-)
 create mode 100644 FrontEnd/index.html
 delete mode 100644 FrontEnd/src/index.ejs
 delete mode 100644 FrontEnd/src/tsconfig.json
 create mode 100644 FrontEnd/tsconfig.json

(limited to 'FrontEnd/src/views/timeline-common/TimelinePostListView.tsx')

diff --git a/BackEnd/Timeline/FrontEndMode.cs b/BackEnd/Timeline/FrontEndMode.cs
index 63503292..eb718028 100644
--- a/BackEnd/Timeline/FrontEndMode.cs
+++ b/BackEnd/Timeline/FrontEndMode.cs
@@ -4,7 +4,6 @@
     {
         Disable,
         Mock,
-        Proxy,
         Normal
     }
 }
diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json
index f683ca2d..7f68b709 100644
--- a/BackEnd/Timeline/Properties/launchSettings.json
+++ b/BackEnd/Timeline/Properties/launchSettings.json
@@ -4,7 +4,7 @@
       "commandName": "Project",
       "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development",
-        "ASPNETCORE_FRONTEND": "Proxy"
+        "ASPNETCORE_FRONTEND": "Disable"
       }
     },
     "Dev-Mock": {
diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs
index 18097e2c..26ffb4b1 100644
--- a/BackEnd/Timeline/Startup.cs
+++ b/BackEnd/Timeline/Startup.cs
@@ -166,10 +166,6 @@ namespace Timeline
             {
                 app.UseSpa(spa =>
                 {
-                    if (_frontEndMode == FrontEndMode.Proxy)
-                    {
-                        spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri);
-                    }
                 });
             }
         }
diff --git a/FrontEnd/index.html b/FrontEnd/index.html
new file mode 100644
index 00000000..87e19743
--- /dev/null
+++ b/FrontEnd/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+
+    <link rel="icon" href="/favicon.ico" />
+    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
+    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
+    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
+    <link rel="manifest" href="/site.webmanifest" />
+    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
+    <meta name="msapplication-TileColor" content="#2d89ef" />
+    <meta name="theme-color" content="#ffffff" />
+
+    <title>Timeline</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>
+        We're sorry but Timeline doesn't work properly without JavaScript
+        enabled. Please enable it to continue.
+      </strong>
+    </noscript>
+    <div id="app"></div>
+    <script type="module" src="/src/index.tsx"></script>
+  </body>
+</html>
diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index 1afb5ad0..5d12899e 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -33,8 +33,9 @@
     "xregexp": "^5.0.2"
   },
   "scripts": {
-    "start": "webpack serve --config ./webpack.config.dev.js",
-    "build": "webpack --config ./webpack.config.prod.js",
+    "start": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview",
     "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx",
     "lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix"
   },
diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js
index 74ee8155..9129aa1f 100644
--- a/FrontEnd/postcss.config.js
+++ b/FrontEnd/postcss.config.js
@@ -1,10 +1,5 @@
+import postcssPresetEnv from "postcss-preset-env";
+
 module.exports = {
-  plugins: [
-    [
-      "postcss-preset-env",
-      {
-        // Options
-      },
-    ],
-  ],
+  plugins: [postcssPresetEnv()],
 };
diff --git a/FrontEnd/src/index.ejs b/FrontEnd/src/index.ejs
deleted file mode 100644
index c2ff4182..00000000
--- a/FrontEnd/src/index.ejs
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
-
-    <link rel="icon" href="/favicon.ico" />
-    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
-    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
-    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
-    <link rel="manifest" href="/site.webmanifest" />
-    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
-    <meta name="msapplication-TileColor" content="#2d89ef" />
-    <meta name="theme-color" content="#ffffff" />
-
-    <title><%= htmlWebpackPlugin.options.title %></title>
-  </head>
-  <body>
-    <noscript>
-      <strong>
-        We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
-        properly without JavaScript enabled. Please enable it to continue.
-      </strong>
-    </noscript>
-    <div id="app"></div>
-    <!-- built files will be auto injected -->
-  </body>
-</html>
diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts
index fe4c7a9a..40279eca 100644
--- a/FrontEnd/src/services/TimelinePostBuilder.ts
+++ b/FrontEnd/src/services/TimelinePostBuilder.ts
@@ -2,8 +2,8 @@ import { Remarkable } from "remarkable";
 
 import { UiLogicError } from "@/common";
 
-import { base64 } from "http/common";
-import { HttpTimelinePostPostRequest } from "http/timeline";
+import { base64 } from "@/http/common";
+import { HttpTimelinePostPostRequest } from "@/http/timeline";
 
 export default class TimelinePostBuilder {
   private _onChange: () => void;
diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts
index 4ebb705d..d8c0ae00 100644
--- a/FrontEnd/src/services/timeline.ts
+++ b/FrontEnd/src/services/timeline.ts
@@ -1,9 +1,9 @@
-import { TimelineVisibility } from "http/timeline";
+import { TimelineVisibility } from "@/http/timeline";
 import XRegExp from "xregexp";
 import { Observable } from "rxjs";
 import { HubConnectionBuilder, HubConnectionState } from "@microsoft/signalr";
 
-import { getHttpToken } from "http/common";
+import { getHttpToken } from "@/http/common";
 
 const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u");
 
diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts
index 3375c88a..9a8e5687 100644
--- a/FrontEnd/src/services/user.ts
+++ b/FrontEnd/src/services/user.ts
@@ -3,12 +3,12 @@ import { BehaviorSubject, Observable } from "rxjs";
 
 import { UiLogicError } from "@/common";
 
-import { HttpNetworkError, setHttpToken } from "http/common";
+import { HttpNetworkError, setHttpToken } from "@/http/common";
 import {
   getHttpTokenClient,
   HttpCreateTokenBadCredentialError,
-} from "http/token";
-import { getHttpUserClient, HttpUser, UserPermission } from "http/user";
+} from "@/http/token";
+import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user";
 
 import { pushAlert } from "./alert";
 
diff --git a/FrontEnd/src/tsconfig.json b/FrontEnd/src/tsconfig.json
deleted file mode 100644
index 817c50bd..00000000
--- a/FrontEnd/src/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "compilerOptions": {
-    "allowJs": true,
-    "skipLibCheck": true,
-    "esModuleInterop": true,
-    "allowSyntheticDefaultImports": true,
-    "strict": true,
-    "forceConsistentCasingInFileNames": true,
-    "module": "esnext",
-    "moduleResolution": "node",
-    "resolveJsonModule": true,
-    "isolatedModules": true,
-    "jsx": "react",
-    "types": ["vite/client"],
-    "sourceMap": true,
-    "baseUrl": "./",
-    "paths": {
-      "@/*": ["*"]
-    },
-    "lib": ["dom", "dom.iterable", "esnext"]
-  },
-  "include": ["."]
-}
diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx
index 4e9cd600..558d3aee 100644
--- a/FrontEnd/src/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/views/admin/UserAdmin.tsx
@@ -12,7 +12,7 @@ import {
   HttpUser,
   kUserPermissionList,
   UserPermission,
-} from "http/user";
+} from "@/http/user";
 import { Trans, useTranslation } from "react-i18next";
 
 interface DialogProps<TData = undefined, TReturn = undefined> {
diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx
index 431d1e9a..f5200415 100644
--- a/FrontEnd/src/views/center/CenterBoards.tsx
+++ b/FrontEnd/src/views/center/CenterBoards.tsx
@@ -5,9 +5,9 @@ import { useTranslation } from "react-i18next";
 import { pushAlert } from "@/services/alert";
 import { useUserLoggedIn } from "@/services/user";
 
-import { getHttpTimelineClient } from "http/timeline";
-import { getHttpBookmarkClient } from "http/bookmark";
-import { getHttpHighlightClient } from "http/highlight";
+import { getHttpTimelineClient } from "@/http/timeline";
+import { getHttpBookmarkClient } from "@/http/bookmark";
+import { getHttpHighlightClient } from "@/http/highlight";
 
 import TimelineBoard from "./TimelineBoard";
 
diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx
index bb80266b..840c0415 100644
--- a/FrontEnd/src/views/center/TimelineBoard.tsx
+++ b/FrontEnd/src/views/center/TimelineBoard.tsx
@@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
 import { useTranslation } from "react-i18next";
 import { Spinner } from "react-bootstrap";
 
-import { HttpTimelineInfo } from "http/timeline";
+import { HttpTimelineInfo } from "@/http/timeline";
 
 import TimelineLogo from "../common/TimelineLogo";
 import UserTimelineLogo from "../common/UserTimelineLogo";
@@ -34,7 +34,7 @@ const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({
   actions,
 }) => {
   const { name, title } = timeline;
-  const isPersonal = name.startsWith("@");
+  const isPersonal = name.startsWith("src");
   const url = isPersonal
     ? `/users/${timeline.owner.username}`
     : `/timelines/${name}`;
diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx
index a2437ae5..b4e25ba1 100644
--- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx
+++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx
@@ -3,7 +3,7 @@ import { useHistory } from "react-router";
 
 import { validateTimelineName } from "@/services/timeline";
 import OperationDialog from "../common/OperationDialog";
-import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
 
 interface TimelineCreateDialogProps {
   open: boolean;
diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx
index 901697db..9e822528 100644
--- a/FrontEnd/src/views/common/user/UserAvatar.tsx
+++ b/FrontEnd/src/views/common/user/UserAvatar.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 
-import { getHttpUserClient } from "http/user";
+import { getHttpUserClient } from "@/http/user";
 
 export interface UserAvatarProps
   extends React.ImgHTMLAttributes<HTMLImageElement> {
diff --git a/FrontEnd/src/views/home/TimelineListView.tsx b/FrontEnd/src/views/home/TimelineListView.tsx
index 975875af..2fb54820 100644
--- a/FrontEnd/src/views/home/TimelineListView.tsx
+++ b/FrontEnd/src/views/home/TimelineListView.tsx
@@ -2,7 +2,7 @@ import React from "react";
 
 import { convertI18nText, I18nText } from "@/common";
 
-import { HttpTimelineInfo } from "http/timeline";
+import { HttpTimelineInfo } from "@/http/timeline";
 import { useTranslation } from "react-i18next";
 import { Link } from "react-router-dom";
 
@@ -13,7 +13,7 @@ interface TimelineListItemProps {
 const TimelineListItem: React.FC<TimelineListItemProps> = ({ timeline }) => {
   const url = React.useMemo(
     () =>
-      timeline.name.startsWith("@")
+      timeline.name.startsWith("src")
         ? `/users/${timeline.owner.username}`
         : `/timelines/${timeline.name}`,
     [timeline]
diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx
index efc364d7..0eca23ee 100644
--- a/FrontEnd/src/views/home/index.tsx
+++ b/FrontEnd/src/views/home/index.tsx
@@ -1,8 +1,8 @@
 import React from "react";
 import { useHistory } from "react-router";
 
-import { HttpTimelineInfo } from "http/timeline";
-import { getHttpHighlightClient } from "http/highlight";
+import { HttpTimelineInfo } from "@/http/timeline";
+import { getHttpHighlightClient } from "@/http/highlight";
 
 import SearchInput from "../common/SearchInput";
 import TimelineListView from "./TimelineListView";
diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx
index 14a9709c..9a26802d 100644
--- a/FrontEnd/src/views/search/index.tsx
+++ b/FrontEnd/src/views/search/index.tsx
@@ -4,9 +4,9 @@ import { Container, Row } from "react-bootstrap";
 import { useHistory, useLocation } from "react-router";
 import { Link } from "react-router-dom";
 
-import { HttpNetworkError } from "http/common";
-import { getHttpSearchClient } from "http/search";
-import { HttpTimelineInfo } from "http/timeline";
+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";
@@ -14,7 +14,7 @@ import UserAvatar from "../common/user/UserAvatar";
 const TimelineSearchResultItemView: React.FC<{
   timeline: HttpTimelineInfo;
 }> = ({ timeline }) => {
-  const link = timeline.name.startsWith("@")
+  const link = timeline.name.startsWith("src")
     ? `users/${timeline.owner.username}`
     : `timelines/${timeline.name}`;
 
@@ -33,7 +33,7 @@ const TimelineSearchResultItemView: React.FC<{
         />
         {timeline.owner.nickname}
         <small className="ms-3 text-secondary">
-          @{timeline.owner.username}
+          src{timeline.owner.username}
         </small>
       </div>
     </div>
diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
index 338d2112..c4f6f492 100644
--- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
@@ -7,7 +7,7 @@ import { UiLogicError } from "@/common";
 
 import { useUserLoggedIn } from "@/services/user";
 
-import { getHttpUserClient } from "http/user";
+import { getHttpUserClient } from "@/http/user";
 
 import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper";
 
diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx
index e6420f36..4b44cdd6 100644
--- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx
+++ b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx
@@ -1,4 +1,4 @@
-import { getHttpUserClient } from "http/user";
+import { getHttpUserClient } from "@/http/user";
 import { useUserLoggedIn } from "@/services/user";
 import React from "react";
 
diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx
index df43d8d2..1b9d6d2a 100644
--- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx
+++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 import classnames from "classnames";
-import { HubConnectionState } from "@microsoft/signalr";
+import { HubConnectionState } from "srcmicrosoft/signalr";
 import { useTranslation } from "react-i18next";
 
 export interface ConnectionStatusBadgeProps {
diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
index 1514d28f..685e17be 100644
--- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
@@ -4,7 +4,7 @@ import { Form, Spinner } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { Prompt } from "react-router";
 
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
 import FlatButton from "../common/FlatButton";
 import TabPages from "../common/TabPages";
diff --git a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx
index 21c5272e..001e52d7 100644
--- a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx
+++ b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
 import OperationDialog from "../common/OperationDialog";
 
diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx
index 40619e64..31ea5870 100644
--- a/FrontEnd/src/views/timeline-common/Timeline.tsx
+++ b/FrontEnd/src/views/timeline-common/Timeline.tsx
@@ -1,12 +1,12 @@
 import React from "react";
-import { HubConnectionState } from "@microsoft/signalr";
+import { HubConnectionState } from "srcmicrosoft/signalr";
 
 import {
   HttpForbiddenError,
   HttpNetworkError,
   HttpNotFoundError,
-} from "http/common";
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline";
+} from "@/http/common";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
 import { getTimelinePostUpdate$ } from "@/services/timeline";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx
index 3d4de8b8..1ef085a7 100644
--- a/FrontEnd/src/views/timeline-common/TimelineMember.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx
@@ -4,12 +4,12 @@ import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap";
 
 import { convertI18nText, I18nText } from "@/common";
 
-import { HttpUser } from "http/user";
-import { getHttpSearchClient } from "http/search";
+import { HttpUser } from "@/http/user";
+import { getHttpSearchClient } from "@/http/search";
 
 import SearchInput from "../common/SearchInput";
 import UserAvatar from "../common/user/UserAvatar";
-import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
 
 const TimelineMemberItem: React.FC<{
   user: HttpUser;
@@ -27,7 +27,7 @@ const TimelineMemberItem: React.FC<{
         <Col>
           <Row>{user.nickname}</Row>
           <Row>
-            <small>{"@" + user.username}</small>
+            <small>{"src" + user.username}</small>
           </Row>
         </Col>
         {onAction ? (
diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
index 038ea3ab..623d643f 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
@@ -2,8 +2,8 @@ import React from "react";
 import classnames from "classnames";
 import { useTranslation } from "react-i18next";
 
-import { getHttpHighlightClient } from "http/highlight";
-import { getHttpBookmarkClient } from "http/bookmark";
+import { getHttpHighlightClient } from "@/http/highlight";
+import { getHttpBookmarkClient } from "@/http/bookmark";
 
 import { useUser } from "@/services/user";
 import { pushAlert } from "@/services/alert";
diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
index 44926cc6..658ce502 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
@@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
 import { Container } from "react-bootstrap";
 import { HubConnectionState } from "@microsoft/signalr";
 
-import { HttpNetworkError, HttpNotFoundError } from "http/common";
-import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline";
+import { HttpNetworkError, HttpNotFoundError } from "@/http/common";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
 
 import { getAlertHost } from "@/services/alert";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx
index d569a2d7..37f02a82 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 
-import { HttpTimelinePostInfo } from "http/timeline";
+import { HttpTimelinePostInfo } from "@/http/timeline";
 
 import useScrollToTop from "@/utilities/useScrollToTop";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx
index f1b53335..607b72c9 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx
@@ -4,8 +4,8 @@ import { Remarkable } from "remarkable";
 
 import { UiLogicError } from "@/common";
 
-import { HttpNetworkError } from "http/common";
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline";
+import { HttpNetworkError } from "@/http/common";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
 import { useUser } from "@/services/user";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
index 0f470fd6..1f9f02a5 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
@@ -10,10 +10,10 @@ import {
   HttpTimelineInfo,
   HttpTimelinePostInfo,
   HttpTimelinePostPostRequestData,
-} from "http/timeline";
+} from "@/http/timeline";
 
 import { pushAlert } from "@/services/alert";
-import { base64 } from "http/common";
+import { base64 } from "@/http/common";
 
 import BlobImage from "../common/BlobImage";
 import LoadingButton from "../common/LoadingButton";
@@ -138,7 +138,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
     (kind === "text" && text.length !== 0) ||
     (kind === "image" && image != null);
 
-  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  // eslint-disable-next-line srctypescript-eslint/no-non-null-assertion
   const containerRef = React.useRef<HTMLDivElement>(null!);
 
   const notifyHeightChange = (): void => {
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx
index 49284720..ba204b72 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx
@@ -1,7 +1,7 @@
 import React, { Fragment } from "react";
 import classnames from "classnames";
 
-import { HttpTimelinePostInfo } from "http/timeline";
+import { HttpTimelinePostInfo } from "@/http/timeline";
 
 import TimelinePostView from "./TimelinePostView";
 import TimelineDateLabel from "./TimelineDateLabel";
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx
index e8b32c71..f7b81478 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx
@@ -3,7 +3,7 @@ import classnames from "classnames";
 import { Link } from "react-router-dom";
 import { useTranslation } from "react-i18next";
 
-import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline";
+import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
 import { pushAlert } from "@/services/alert";
 
diff --git a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx
index 83b24d01..70f72025 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx
@@ -6,7 +6,7 @@ import {
   HttpTimelinePatchRequest,
   kTimelineVisibilities,
   TimelineVisibility,
-} from "http/timeline";
+} from "@/http/timeline";
 
 import OperationDialog from "../common/OperationDialog";
 
diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx
index e031b565..86063843 100644
--- a/FrontEnd/src/views/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/views/timeline/TimelineCard.tsx
@@ -29,7 +29,7 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
               />
               {timeline.owner.nickname}
               <small className="ms-3 text-secondary">
-                @{timeline.owner.username}
+                src{timeline.owner.username}
               </small>
             </div>
           </>
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
index 8821507d..dbca62ca 100644
--- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
+++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
@@ -2,7 +2,7 @@ import React from "react";
 import { useHistory } from "react-router";
 import { Trans } from "react-i18next";
 
-import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
 
 import OperationDialog from "../common/OperationDialog";
 
diff --git a/FrontEnd/src/views/user/index.tsx b/FrontEnd/src/views/user/index.tsx
index 57454d0d..0013b254 100644
--- a/FrontEnd/src/views/user/index.tsx
+++ b/FrontEnd/src/views/user/index.tsx
@@ -14,7 +14,7 @@ const UserPage: React.FC = () => {
   return (
     <>
       <TimelinePageTemplate
-        timelineName={`@${username}`}
+        timelineName={`src${username}`}
         notFoundI18nKey="timeline.userNotExist"
         reloadKey={reloadKey}
         onReload={() => setReloadKey(reloadKey + 1)}
diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json
new file mode 100644
index 00000000..3afe2c3e
--- /dev/null
+++ b/FrontEnd/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "lib": ["DOM", "DOM.Iterable", "ESNext"],
+    "allowJs": false,
+    "skipLibCheck": false,
+    "esModuleInterop": false,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "react",
+    "noEmit": true,
+    "types": ["vite/client"],
+    "sourceMap": true,
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["./src"]
+}
diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js
index 6e8cde1b..2e85c36a 100644
--- a/FrontEnd/vite.config.js
+++ b/FrontEnd/vite.config.js
@@ -7,6 +7,9 @@ import { defineConfig } from "vite";
 
 export default defineConfig({
   plugins: [reactRefresh()],
+  resolve: {
+    alias: [{ find: "@", replacement: "/src" }],
+  },
   server: {
     port: 13000,
     proxy: {
-- 
cgit v1.2.3

