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

---
 FrontEnd/src/views/admin/Admin.tsx | 48 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)
 create mode 100644 FrontEnd/src/views/admin/Admin.tsx

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

diff --git a/FrontEnd/src/views/admin/Admin.tsx b/FrontEnd/src/views/admin/Admin.tsx
new file mode 100644
index 00000000..0b6d1f05
--- /dev/null
+++ b/FrontEnd/src/views/admin/Admin.tsx
@@ -0,0 +1,48 @@
+import React, { Fragment } from "react";
+import { Redirect, Route, Switch, useRouteMatch, match } from "react-router";
+import { Container } from "react-bootstrap";
+import { useTranslation } from "react-i18next";
+
+import { AuthUser } from "@/services/user";
+
+import AdminNav from "./AdminNav";
+import UserAdmin from "./UserAdmin";
+import MoreAdmin from "./MoreAdmin";
+
+interface AdminProps {
+  user: AuthUser;
+}
+
+const Admin: React.FC<AdminProps> = ({ user }) => {
+  useTranslation("admin");
+
+  const match = useRouteMatch();
+
+  return (
+    <Fragment>
+      <Switch>
+        <Redirect from={match.path} to={`${match.path}/users`} exact />
+        <Route path={`${match.path}/:name`}>
+          {(p) => {
+            const match = p.match as match<{ name: string }>;
+            const name = match.params["name"];
+            return (
+              <Container>
+                <AdminNav />
+                {(() => {
+                  if (name === "users") {
+                    return <UserAdmin user={user} />;
+                  } else if (name === "more") {
+                    return <MoreAdmin user={user} />;
+                  }
+                })()}
+              </Container>
+            );
+          }}
+        </Route>
+      </Switch>
+    </Fragment>
+  );
+};
+
+export default Admin;
-- 
cgit v1.2.3


From ddce03a67708249eec129eb36744be460345bd75 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Tue, 15 Jun 2021 16:23:44 +0800
Subject: ...

---
 FrontEnd/.eslintrc.js                              |   2 +-
 FrontEnd/package.json                              |  15 --
 FrontEnd/postcss.config.js                         |   5 -
 FrontEnd/src/index.css                             | 155 +++++++++++
 FrontEnd/src/index.sass                            | 120 ---------
 FrontEnd/src/index.tsx                             |   8 +-
 FrontEnd/src/palette.ts                            |   2 +
 FrontEnd/src/views/about/about.sass                |   4 -
 FrontEnd/src/views/about/index.css                 |   4 +
 FrontEnd/src/views/about/index.tsx                 |   2 +
 FrontEnd/src/views/admin/Admin.tsx                 |   2 +
 FrontEnd/src/views/admin/admin.sass                |  22 --
 FrontEnd/src/views/admin/index.css                 |  19 ++
 FrontEnd/src/views/center/TimelineBoard.tsx        |  17 +-
 FrontEnd/src/views/center/center.sass              |  36 ---
 FrontEnd/src/views/center/index.css                |  73 ++++++
 FrontEnd/src/views/center/index.tsx                |   2 +
 FrontEnd/src/views/common/AppBar.tsx               |   2 +
 FrontEnd/src/views/common/FlatButton.tsx           |  36 ---
 FrontEnd/src/views/common/button/FlatButton.css    |  45 ++++
 FrontEnd/src/views/common/button/FlatButton.tsx    |  36 +++
 FrontEnd/src/views/common/common.sass              | 191 --------------
 FrontEnd/src/views/common/index.css                | 273 +++++++++++++++++++
 FrontEnd/src/views/home/home.sass                  |  29 ---
 FrontEnd/src/views/home/index.css                  |  73 ++++++
 FrontEnd/src/views/home/index.tsx                  |   2 +
 FrontEnd/src/views/login/index.css                 |   3 +
 FrontEnd/src/views/login/index.tsx                 |   2 +
 FrontEnd/src/views/login/login.sass                |   2 -
 FrontEnd/src/views/search/index.css                |  15 ++
 FrontEnd/src/views/search/index.tsx                |   2 +
 FrontEnd/src/views/search/search.sass              |  13 -
 FrontEnd/src/views/settings/index.css              |  24 ++
 FrontEnd/src/views/settings/index.tsx              |   2 +
 FrontEnd/src/views/settings/settings.sass          |  14 -
 FrontEnd/src/views/timeline-common/Timeline.tsx    |   4 +-
 FrontEnd/src/views/timeline-common/index.css       | 289 +++++++++++++++++++++
 .../src/views/timeline-common/timeline-common.sass | 259 ------------------
 FrontEnd/src/views/timeline/timeline.sass          |   0
 FrontEnd/src/views/user/index.css                  |   9 +
 FrontEnd/src/views/user/index.tsx                  |   2 +
 FrontEnd/src/views/user/user.sass                  |   7 -
 42 files changed, 1054 insertions(+), 768 deletions(-)
 delete mode 100644 FrontEnd/postcss.config.js
 create mode 100644 FrontEnd/src/index.css
 delete mode 100644 FrontEnd/src/index.sass
 delete mode 100644 FrontEnd/src/views/about/about.sass
 create mode 100644 FrontEnd/src/views/about/index.css
 delete mode 100644 FrontEnd/src/views/admin/admin.sass
 create mode 100644 FrontEnd/src/views/admin/index.css
 delete mode 100644 FrontEnd/src/views/center/center.sass
 create mode 100644 FrontEnd/src/views/center/index.css
 delete mode 100644 FrontEnd/src/views/common/FlatButton.tsx
 create mode 100644 FrontEnd/src/views/common/button/FlatButton.css
 create mode 100644 FrontEnd/src/views/common/button/FlatButton.tsx
 delete mode 100644 FrontEnd/src/views/common/common.sass
 create mode 100644 FrontEnd/src/views/common/index.css
 delete mode 100644 FrontEnd/src/views/home/home.sass
 create mode 100644 FrontEnd/src/views/home/index.css
 create mode 100644 FrontEnd/src/views/login/index.css
 delete mode 100644 FrontEnd/src/views/login/login.sass
 create mode 100644 FrontEnd/src/views/search/index.css
 delete mode 100644 FrontEnd/src/views/search/search.sass
 create mode 100644 FrontEnd/src/views/settings/index.css
 delete mode 100644 FrontEnd/src/views/settings/settings.sass
 create mode 100644 FrontEnd/src/views/timeline-common/index.css
 delete mode 100644 FrontEnd/src/views/timeline-common/timeline-common.sass
 delete mode 100644 FrontEnd/src/views/timeline/timeline.sass
 create mode 100644 FrontEnd/src/views/user/index.css
 delete mode 100644 FrontEnd/src/views/user/user.sass

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

diff --git a/FrontEnd/.eslintrc.js b/FrontEnd/.eslintrc.js
index 611b965f..93b98978 100644
--- a/FrontEnd/.eslintrc.js
+++ b/FrontEnd/.eslintrc.js
@@ -18,7 +18,7 @@ module.exports = {
   },
   parser: "@typescript-eslint/parser",
   parserOptions: {
-    project: ["./src/tsconfig.json"],
+    project: ["./tsconfig.json"],
     ecmaFeatures: {
       jsx: true,
     },
diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index 5d12899e..38dd85ff 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -39,18 +39,6 @@
     "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx",
     "lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix"
   },
-  "browserslist": {
-    "production": [
-      ">0.2%",
-      "not dead",
-      "not op_mini all"
-    ],
-    "development": [
-      "last 1 chrome version",
-      "last 1 firefox version",
-      "last 1 safari version"
-    ]
-  },
   "devDependencies": {
     "@types/color": "^3.0.1",
     "@types/lodash": "^4.14.170",
@@ -70,10 +58,7 @@
     "eslint-plugin-prettier": "^3.4.0",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.0",
-    "postcss": "^8.3.0",
-    "postcss-preset-env": "^6.7.0",
     "prettier": "^2.3.1",
-    "sass": "^1.34.1",
     "typescript": "^4.3.2",
     "vite": "^2.3.7"
   }
diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js
deleted file mode 100644
index 9129aa1f..00000000
--- a/FrontEnd/postcss.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import postcssPresetEnv from "postcss-preset-env";
-
-module.exports = {
-  plugins: [postcssPresetEnv()],
-};
diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css
new file mode 100644
index 00000000..ca0d4829
--- /dev/null
+++ b/FrontEnd/src/index.css
@@ -0,0 +1,155 @@
+.tl-color-primary {
+  color: var(--tl-primary-color);
+}
+
+.tl-color-danger {
+  color: var(--tl-danger-color);
+}
+
+small {
+  line-height: 1.2;
+}
+
+.flex-fix-length {
+  flex-grow: 0;
+  flex-shrink: 0;
+}
+
+.avatar {
+  width: 60px;
+  height: 60px;
+}
+
+.avatar.large {
+  width: 100px;
+  height: 100px;
+}
+
+.avatar.small {
+  width: 40px;
+  height: 40px;
+}
+
+.icon-button {
+  font-size: 1.4rem;
+  cursor: pointer;
+}
+
+.icon-button.large {
+  font-size: 1.6rem;
+}
+
+
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+textarea {
+  resize: none;
+}
+
+.white-space-no-wrap {
+  white-space: nowrap;
+}
+
+.cru-card {
+  border: 1px solid;
+  border-color: #e9ecef;
+  background: #f8f9fa;
+  transition: all 0.3s;
+}
+
+.cru-card:hover {
+  border-color: var(--tl-primary-color);
+}
+
+.full-viewport-center-child {
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.text-orange {
+  color: #fd7e14;
+}
+
+.text-yellow {
+  color: #ffc107;
+}
+
+.text-button {
+  background: transparent;
+  border: none;
+}
+.text-button.primary {
+  color: #0d6efd;
+}
+.text-button.primary:hover {
+  color: #599bfe;
+}
+.text-button.secondary {
+  color: #6c757d;
+}
+.text-button.secondary:hover {
+  color: #939ba2;
+}
+.text-button.success {
+  color: #198754;
+}
+.text-button.success:hover {
+  color: #25c87c;
+}
+.text-button.info {
+  color: #0dcaf0;
+}
+.text-button.info:hover {
+  color: #54dbf6;
+}
+.text-button.warning {
+  color: #ffc107;
+}
+.text-button.warning:hover {
+  color: #ffd454;
+}
+.text-button.danger {
+  color: #dc3545;
+}
+.text-button.danger:hover {
+  color: #e77681;
+}
+.text-button.light {
+  color: #f8f9fa;
+}
+.text-button.light:hover {
+  color: white;
+}
+.text-button.dark {
+  color: #212529;
+}
+.text-button.dark:hover {
+  color: #434b53;
+}
+
+.touch-action-none {
+  touch-action: none;
+}
+
+i {
+  line-height: 1;
+}
+
+.markdown-container {
+  white-space: initial;
+}
+.markdown-container img {
+  max-height: 200px;
+  max-width: 100%;
+}
+
+a {
+  text-decoration: none;
+}
diff --git a/FrontEnd/src/index.sass b/FrontEnd/src/index.sass
deleted file mode 100644
index 4cee155f..00000000
--- a/FrontEnd/src/index.sass
+++ /dev/null
@@ -1,120 +0,0 @@
-@import 'bootstrap/scss/bootstrap'
-@import 'bootstrap-icons/font/bootstrap-icons.css'
-
-@import './views/common/common'
-@import './views/common/alert/alert'
-@import './views/center/center'
-@import './views/home/home'
-@import './views/about/about'
-@import './views/login/login'
-@import './views/settings/settings'
-@import './views/timeline-common/timeline-common'
-@import './views/timeline/timeline'
-@import './views/user/user'
-@import './views/search/search'
-
-@import './views/admin/admin'
-
-.tl-color-primary
-  color: var(--tl-primary-color)
-
-.tl-color-danger
-  color: var(--tl-danger-color)
-
-small
-  line-height: 1.2
-
-.flex-fix-length
-  flex-grow: 0
-  flex-shrink: 0
-
-.position-lt
-  left: 0
-  top: 0
-
-.avatar
-  width: 60px
-  height: 60px
-  &.large
-    width: 100px
-    height: 100px
-  &.small
-    width: 40px
-    height: 40px
-
-.icon-button
-  font-size: 1.4rem
-  cursor: pointer
-  &.large
-    font-size: 1.6rem
-
-.flat-button
-  cursor: pointer
-  padding: 0.2em 0.5em
-  border-radius: 0.2em
-  &:hover:not(.disabled)
-    background-color: $gray-200
-  &.disabled
-    cursor: default
-  @each $color, $value in $theme-colors
-    &.#{$color}
-      color: $value
-      &.disabled
-        color: adjust-color($value, $lightness: +15%)
-
-.cursor-pointer
-  cursor: pointer
-
-textarea
-  resize: none
-
-.white-space-no-wrap
-  white-space: nowrap
-
-.cru-card
-  @extend .shadow
-  @extend .rounded
-  border: 1px solid
-  border-color: $gray-200
-  background: $gray-100
-  transition: all 0.3s
-  &:hover
-    border-color: var(--tl-primary-color)
-
-.full-viewport-center-child
-  position: fixed
-  width: 100vw
-  height: 100vh
-  display: flex
-  justify-content: center
-  align-items: center
-
-.text-orange
-  color: $orange
-
-.text-yellow
-  color: $yellow
-
-.text-button
-  background: transparent
-  border: none
-  @each $color, $value in $theme-colors
-    &.#{$color}
-      color: $value
-      &:hover
-        color: adjust-color($value, $lightness: +15%)
-
-.touch-action-none
-  touch-action: none
-
-i
-  line-height: 1
-
-.markdown-container
-  white-space: initial
-  img
-    max-height: 200px
-    max-width: 100%
-
-a
-  text-decoration: none
diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx
index fb0c8899..83c25792 100644
--- a/FrontEnd/src/index.tsx
+++ b/FrontEnd/src/index.tsx
@@ -3,17 +3,19 @@ import "core-js/modules/es.promise";
 import "core-js/modules/es.array.iterator";
 import "pepjs";
 
+import "bootstrap/dist/css/bootstrap.css";
+import "bootstrap-icons/font/bootstrap-icons.css";
+
 import React from "react";
 import ReactDOM from "react-dom";
 
-import "./index.sass";
+import "./index.css";
 
 import "./i18n";
+import "./palette";
 
 import App from "./App";
 
-import "./palette";
-
 import { userService } from "./services/user";
 
 void userService.checkLoginState();
diff --git a/FrontEnd/src/palette.ts b/FrontEnd/src/palette.ts
index c4f4f4f9..11b3de85 100644
--- a/FrontEnd/src/palette.ts
+++ b/FrontEnd/src/palette.ts
@@ -30,6 +30,8 @@ export interface Palette {
   [key: string]: PaletteColor;
 }
 
+export type PaletteColorType = keyof Palette;
+
 export function generatePaletteColor(color: string): PaletteColor {
   const c = Color(color);
   return {
diff --git a/FrontEnd/src/views/about/about.sass b/FrontEnd/src/views/about/about.sass
deleted file mode 100644
index f4d00cae..00000000
--- a/FrontEnd/src/views/about/about.sass
+++ /dev/null
@@ -1,4 +0,0 @@
-.about-link-icon
-  @extend .mx-2
-  width: 1.2em
-  height: 1.2em
diff --git a/FrontEnd/src/views/about/index.css b/FrontEnd/src/views/about/index.css
new file mode 100644
index 00000000..2574f4b7
--- /dev/null
+++ b/FrontEnd/src/views/about/index.css
@@ -0,0 +1,4 @@
+.about-link-icon {
+  width: 1.2em;
+  height: 1.2em;
+}
diff --git a/FrontEnd/src/views/about/index.tsx b/FrontEnd/src/views/about/index.tsx
index a8a53a97..db4814c4 100644
--- a/FrontEnd/src/views/about/index.tsx
+++ b/FrontEnd/src/views/about/index.tsx
@@ -4,6 +4,8 @@ import { useTranslation, Trans } from "react-i18next";
 import authorAvatarUrl from "./author-avatar.png";
 import githubLogoUrl from "./github.png";
 
+import "./index.css";
+
 const frontendCredits: {
   name: string;
   url: string;
diff --git a/FrontEnd/src/views/admin/Admin.tsx b/FrontEnd/src/views/admin/Admin.tsx
index 0b6d1f05..34e7e2f6 100644
--- a/FrontEnd/src/views/admin/Admin.tsx
+++ b/FrontEnd/src/views/admin/Admin.tsx
@@ -9,6 +9,8 @@ import AdminNav from "./AdminNav";
 import UserAdmin from "./UserAdmin";
 import MoreAdmin from "./MoreAdmin";
 
+import "./index.css";
+
 interface AdminProps {
   user: AuthUser;
 }
diff --git a/FrontEnd/src/views/admin/admin.sass b/FrontEnd/src/views/admin/admin.sass
deleted file mode 100644
index 1ce010f8..00000000
--- a/FrontEnd/src/views/admin/admin.sass
+++ /dev/null
@@ -1,22 +0,0 @@
-.admin-user-item
-  position: relative
-
-  .edit-mask
-    position: absolute
-    top: 0
-    left: 0
-    bottom: 0
-    right: 0
-
-    background: #ffffffc5
-    position: absolute
-
-    display: flex
-    justify-content: center
-    align-items: center
-
-    @include media-breakpoint-down(xs)
-      flex-direction: column
-
-    button
-      margin: 0.5em 2em
diff --git a/FrontEnd/src/views/admin/index.css b/FrontEnd/src/views/admin/index.css
new file mode 100644
index 00000000..00917600
--- /dev/null
+++ b/FrontEnd/src/views/admin/index.css
@@ -0,0 +1,19 @@
+.admin-user-item {
+  position: relative;
+}
+.admin-user-item .edit-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: #ffffffc5;
+  position: absolute;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.admin-user-item .edit-mask button {
+  margin: 0.5em 2em;
+}
diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx
index 840c0415..a6a60b3d 100644
--- a/FrontEnd/src/views/center/TimelineBoard.tsx
+++ b/FrontEnd/src/views/center/TimelineBoard.tsx
@@ -9,6 +9,7 @@ import { HttpTimelineInfo } from "@/http/timeline";
 import TimelineLogo from "../common/TimelineLogo";
 import UserTimelineLogo from "../common/UserTimelineLogo";
 import LoadFailReload from "../common/LoadFailReload";
+import FlatButton from "../common/button/FlatButton";
 
 interface TimelineBoardItemProps {
   timeline: HttpTimelineInfo;
@@ -231,23 +232,19 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
         {title != null && <h3>{title}</h3>}
         {editable &&
           (editing ? (
-            <div
-              className="flat-button text-primary"
+            <FlatButton
+              text="done"
               onClick={() => {
                 setEditing(false);
               }}
-            >
-              {t("done")}
-            </div>
+            />
           ) : (
-            <div
-              className="flat-button text-primary"
+            <FlatButton
+              text="edit"
               onClick={() => {
                 setEditing(true);
               }}
-            >
-              {t("edit")}
-            </div>
+            />
           ))}
       </div>
       {(() => {
diff --git a/FrontEnd/src/views/center/center.sass b/FrontEnd/src/views/center/center.sass
deleted file mode 100644
index c0dfb9c0..00000000
--- a/FrontEnd/src/views/center/center.sass
+++ /dev/null
@@ -1,36 +0,0 @@
-.timeline-board
-  @extend .cru-card
-  @extend .d-flex
-  @extend .flex-column
-  @extend .py-3
-  min-height: 200px
-  height: 100%
-  position: relative
-
-.timeline-board-header
-  @extend .px-3
-  display: flex
-  align-items: center
-  justify-content: space-between
-
-.timeline-board-item
-  font-size: 1.1em
-  @extend .px-3
-  height: 48px
-  transition: background 0.3s
-  display: flex
-  align-items: center
-  .icon
-    height: 1.3em
-    color: black
-    @extend .me-2
-  &:hover
-    background: $gray-300
-  .right
-    display: flex
-    align-items: center
-    flex-shrink: 0
-  .title
-    white-space: nowrap
-    overflow: hidden
-    text-overflow: ellipsis
diff --git a/FrontEnd/src/views/center/index.css b/FrontEnd/src/views/center/index.css
new file mode 100644
index 00000000..516aba52
--- /dev/null
+++ b/FrontEnd/src/views/center/index.css
@@ -0,0 +1,73 @@
+.timeline-board {
+  min-height: 200px;
+  height: 100%;
+  position: relative;
+}
+
+.timeline-board-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.timeline-board-item {
+  font-size: 1.1em;
+  height: 48px;
+  transition: background 0.3s;
+  display: flex;
+  align-items: center;
+}
+.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;
+}
+
+.home-timeline-list-item {
+  display: flex;
+  align-items: center;
+}
+
+.home-timeline-list-item-timeline {
+  transition: background 0.8s;
+  animation: 0.8s home-timeline-list-item-timeline-enter;
+}
+.home-timeline-list-item-timeline:hover {
+  background: #e9ecef;
+}
+
+@keyframes home-timeline-list-item-timeline-enter {
+  from {
+    transform: translate(-100%, 0);
+    opacity: 0;
+  }
+}
+.home-timeline-list-item-line {
+  width: 80px;
+  flex-shrink: 0;
+}
+
+@keyframes home-timeline-list-loading-head-animation {
+  from {
+    transform: translate(0, -30px);
+    opacity: 1;
+  }
+  to {
+    opacity: 0;
+  }
+}
+.home-timeline-list-loading-head {
+  animation: 1s infinite home-timeline-list-loading-head-animation;
+}
diff --git a/FrontEnd/src/views/center/index.tsx b/FrontEnd/src/views/center/index.tsx
index 0a2abb2c..28d8b372 100644
--- a/FrontEnd/src/views/center/index.tsx
+++ b/FrontEnd/src/views/center/index.tsx
@@ -9,6 +9,8 @@ import SearchInput from "../common/SearchInput";
 import CenterBoards from "./CenterBoards";
 import TimelineCreateDialog from "./TimelineCreateDialog";
 
+import "./index.css";
+
 const HomePage: React.FC = () => {
   const history = useHistory();
 
diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx
index 91dfbee9..ebc8bf0c 100644
--- a/FrontEnd/src/views/common/AppBar.tsx
+++ b/FrontEnd/src/views/common/AppBar.tsx
@@ -9,6 +9,8 @@ import { useUser } from "@/services/user";
 import TimelineLogo from "./TimelineLogo";
 import UserAvatar from "./user/UserAvatar";
 
+import "./index.css";
+
 const AppBar: React.FC = (_) => {
   const { t } = useTranslation();
 
diff --git a/FrontEnd/src/views/common/FlatButton.tsx b/FrontEnd/src/views/common/FlatButton.tsx
deleted file mode 100644
index b1f7a051..00000000
--- a/FrontEnd/src/views/common/FlatButton.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-
-import { BootstrapThemeColor } from "@/common";
-
-export interface FlatButtonProps {
-  variant?: BootstrapThemeColor | string;
-  disabled?: boolean;
-  className?: string;
-  style?: React.CSSProperties;
-  onClick?: () => void;
-}
-
-const FlatButton: React.FC<FlatButtonProps> = (props) => {
-  const { disabled, className, style } = props;
-  const variant = props.variant ?? "primary";
-
-  const onClick = disabled ? undefined : props.onClick;
-
-  return (
-    <div
-      className={classnames(
-        "flat-button",
-        variant,
-        disabled ? "disabled" : null,
-        className
-      )}
-      style={style}
-      onClick={onClick}
-    >
-      {props.children}
-    </div>
-  );
-};
-
-export default FlatButton;
diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css
new file mode 100644
index 00000000..779e3562
--- /dev/null
+++ b/FrontEnd/src/views/common/button/FlatButton.css
@@ -0,0 +1,45 @@
+.cru-flat-button {
+  cursor: pointer;
+  padding: 0.2em 0.5em;
+  border-radius: 0.2em;
+}
+
+.cru-flat-button:hover:not(.disabled) {
+  background-color: #e9ecef;
+}
+
+.cru-flat-button.disabled {
+  cursor: default;
+}
+
+.cru-flat-button.primary {
+  color: var(--tl-primary-color);
+}
+
+.cru-flat-button.primary.disabled {
+  color: var(--tl-primary-lighter-color);
+}
+
+.cru-flat-button.secondary {
+  color: var(--tl-secondary-color);
+}
+
+.cru-flat-button.secondary.disabled {
+  color: var(--tl-secondary-lighter-color);
+}
+
+.cru-flat-button.success {
+  color: var(--tl-success-color);
+}
+
+.cru-flat-button.success.disabled {
+  color: var(--tl-success-lighter-color);
+}
+
+.cru-flat-button.danger {
+  color: var(--tl-danger-color);
+}
+
+.cru-flat-button.danger.disabled {
+  color: var(--tl-danger-ligher-color);
+}
diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx
new file mode 100644
index 00000000..0727eb88
--- /dev/null
+++ b/FrontEnd/src/views/common/button/FlatButton.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+
+import { convertI18nText, I18nText } from "@/common";
+import { PaletteColorType } from "@/palette";
+
+import "./FlatButton.css";
+import classNames from "classnames";
+
+function _FlatButton(
+  {
+    text,
+    color,
+    onClick,
+  }: {
+    text: I18nText;
+    color?: PaletteColorType;
+    onClick?: () => void;
+  },
+  ref: React.ForwardedRef<HTMLButtonElement>
+): React.ReactElement | null {
+  const { t } = useTranslation();
+
+  return (
+    <button
+      ref={ref}
+      className={classNames("cru-flat-button", color ?? "primary")}
+      onClick={onClick}
+    >
+      {convertI18nText(text, t)}
+    </button>
+  );
+}
+
+const FlatButton = React.forwardRef(_FlatButton);
+export default FlatButton;
diff --git a/FrontEnd/src/views/common/common.sass b/FrontEnd/src/views/common/common.sass
deleted file mode 100644
index cbf7292e..00000000
--- a/FrontEnd/src/views/common/common.sass
+++ /dev/null
@@ -1,191 +0,0 @@
-.image-cropper-container
-  position: relative
-  box-sizing: border-box
-  user-select: none
-
-.image-cropper-container img
-  position: absolute
-  left: 0
-  top: 0
-  width: 100%
-  height: 100%
-
-.image-cropper-mask-container
-  position: absolute
-  left: 0
-  top: 0
-  right: 0
-  bottom: 0
-  overflow: hidden
-
-.image-cropper-mask
-  position: absolute
-  box-shadow: 0 0 0 10000px rgba(255, 255, 255, 80%)
-  touch-action: none
-
-.image-cropper-handler
-  position: absolute
-  width: 26px
-  height: 26px
-  border: black solid 2px
-  border-radius: 50%
-  background: white
-  touch-action: none
-
-.app-bar
-  display: flex
-  align-items: center
-  height: 56px
-
-  position: fixed
-  z-index: 1030
-  top: 0
-  left: 0
-  right: 0
-
-  background-color: var(--tl-primary-color)
-
-  transition: background-color 1s
-
-  a
-    color: var(--tl-text-on-primary-inactive-color)
-    text-decoration: none
-    margin: 0 1em
-
-    &:hover
-      color: var(--tl-text-on-primary-color)
-
-    &.active
-      color: var(--tl-text-on-primary-color)
-
-.app-bar-brand
-  display: flex
-  align-items: center
-
-.app-bar-brand-icon
-  height: 2em
-
-.app-bar-main-area
-  display: flex
-  flex-grow: 1
-
-.app-bar-link-area
-  display: flex
-  align-items: center
-  flex-shrink: 0
-
-.app-bar-user-area
-  display: flex
-  align-items: center
-  flex-shrink: 0
-  margin-left: auto
-
-.small-screen
-  .app-bar-main-area
-    position: absolute
-    top: 56px
-    left: 0
-    right: 0
-
-    transform-origin: top
-    transition: transform 0.6s, background-color 1s
-
-    background-color: var(--tl-primary-color)
-
-    flex-direction: column
-
-    &.app-bar-collapse
-      transform: scale(1,0)
-
-    a
-      text-align: left
-      padding: 0.5em 0.5em
-
-  .app-bar-link-area
-    flex-direction: column
-    align-items: stretch
-
-  .app-bar-user-area
-    flex-direction: column
-    align-items: stretch
-    margin-left: unset
-
-  .app-bar-avatar
-    align-self: flex-end
-
-.app-bar-toggler
-  margin-left: auto
-  font-size: 2em
-  margin-right: 1em
-  color: var(--tl-text-on-primary-color)
-  cursor: pointer
-  user-select: none
-
-.cru-skeleton
-  padding: 0 1em
-
-.cru-skeleton-line
-  height: 1em
-  background-color: #e6e6e6
-  margin: 0.7em 0
-  border-radius: 0.2em
-
-  &.last
-    width: 50%
-
-.cru-full-page
-  position: fixed
-  z-index: 1031
-  left: 0
-  top: 0
-  right: 0
-  bottom: 0
-  background-color: white
-  padding-top: 56px
-
-.cru-full-page-top-bar
-  height: 56px
-
-  position: absolute
-  top: 0
-  left: 0
-  right: 0
-  z-index: 1
-
-  background-color: var(--tl-primary-color)
-
-  display: flex
-  align-items: center
-
-.cru-full-page-content-container
-  overflow: scroll
-
-.cru-menu
-  min-width: 200px
-
-.cru-menu-item
-  font-size: 1.2em
-  padding: 0.5em 1.5em
-  cursor: pointer
-
-  @each $color, $value in $theme-colors
-    &.color-#{$color}
-      color: $value
-
-      &:hover
-        color: white
-        background-color: $value
-
-.cru-menu-item-icon
-  margin-right: 1em
-
-.cru-menu-divider
-  border-top: 1px solid $gray-200
-
-.cru-tab-pages-action-area
-  display: flex
-  align-items: center
-
-.cru-search-input
-  display: flex
-  flex-wrap: wrap
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
new file mode 100644
index 00000000..bfd82b58
--- /dev/null
+++ b/FrontEnd/src/views/common/index.css
@@ -0,0 +1,273 @@
+.image-cropper-container {
+  position: relative;
+  box-sizing: border-box;
+  user-select: none;
+}
+
+.image-cropper-container img {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.image-cropper-mask-container {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+}
+
+.image-cropper-mask {
+  position: absolute;
+  box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
+  touch-action: none;
+}
+
+.image-cropper-handler {
+  position: absolute;
+  width: 26px;
+  height: 26px;
+  border: black solid 2px;
+  border-radius: 50%;
+  background: white;
+  touch-action: none;
+}
+
+.app-bar {
+  display: flex;
+  align-items: center;
+  height: 56px;
+  position: fixed;
+  z-index: 1030;
+  top: 0;
+  left: 0;
+  right: 0;
+  background-color: var(--tl-primary-color);
+  transition: background-color 1s;
+}
+.app-bar a {
+  color: var(--tl-text-on-primary-inactive-color);
+  text-decoration: none;
+  margin: 0 1em;
+}
+.app-bar a:hover {
+  color: var(--tl-text-on-primary-color);
+}
+.app-bar a.active {
+  color: var(--tl-text-on-primary-color);
+}
+
+.app-bar-brand {
+  display: flex;
+  align-items: center;
+}
+
+.app-bar-brand-icon {
+  height: 2em;
+}
+
+.app-bar-main-area {
+  display: flex;
+  flex-grow: 1;
+}
+
+.app-bar-link-area {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.app-bar-user-area {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  margin-left: auto;
+}
+
+.small-screen .app-bar-main-area {
+  position: absolute;
+  top: 56px;
+  left: 0;
+  right: 0;
+  transform-origin: top;
+  transition: transform 0.6s, background-color 1s;
+  background-color: var(--tl-primary-color);
+  flex-direction: column;
+}
+.small-screen .app-bar-main-area.app-bar-collapse {
+  transform: scale(1, 0);
+}
+.small-screen .app-bar-main-area a {
+  text-align: left;
+  padding: 0.5em 0.5em;
+}
+.small-screen .app-bar-link-area {
+  flex-direction: column;
+  align-items: stretch;
+}
+.small-screen .app-bar-user-area {
+  flex-direction: column;
+  align-items: stretch;
+  margin-left: unset;
+}
+.small-screen .app-bar-avatar {
+  align-self: flex-end;
+}
+
+.app-bar-toggler {
+  margin-left: auto;
+  font-size: 2em;
+  margin-right: 1em;
+  color: var(--tl-text-on-primary-color);
+  cursor: pointer;
+  user-select: none;
+}
+
+.cru-skeleton {
+  padding: 0 1em;
+}
+
+.cru-skeleton-line {
+  height: 1em;
+  background-color: #e6e6e6;
+  margin: 0.7em 0;
+  border-radius: 0.2em;
+}
+.cru-skeleton-line.last {
+  width: 50%;
+}
+
+.cru-full-page {
+  position: fixed;
+  z-index: 1031;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background-color: white;
+  padding-top: 56px;
+}
+
+.cru-full-page-top-bar {
+  height: 56px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1;
+  background-color: var(--tl-primary-color);
+  display: flex;
+  align-items: center;
+}
+
+.cru-full-page-content-container {
+  overflow: scroll;
+}
+
+.cru-menu {
+  min-width: 200px;
+}
+
+.cru-menu-item {
+  font-size: 1.2em;
+  padding: 0.5em 1.5em;
+  cursor: pointer;
+}
+.cru-menu-item.color-primary {
+  color: #0d6efd;
+}
+.cru-menu-item.color-primary:hover {
+  color: white;
+  background-color: #0d6efd;
+}
+.cru-menu-item.color-secondary {
+  color: #6c757d;
+}
+.cru-menu-item.color-secondary:hover {
+  color: white;
+  background-color: #6c757d;
+}
+.cru-menu-item.color-success {
+  color: #198754;
+}
+.cru-menu-item.color-success:hover {
+  color: white;
+  background-color: #198754;
+}
+.cru-menu-item.color-info {
+  color: #0dcaf0;
+}
+.cru-menu-item.color-info:hover {
+  color: white;
+  background-color: #0dcaf0;
+}
+.cru-menu-item.color-warning {
+  color: #ffc107;
+}
+.cru-menu-item.color-warning:hover {
+  color: white;
+  background-color: #ffc107;
+}
+.cru-menu-item.color-danger {
+  color: #dc3545;
+}
+.cru-menu-item.color-danger:hover {
+  color: white;
+  background-color: #dc3545;
+}
+.cru-menu-item.color-light {
+  color: #f8f9fa;
+}
+.cru-menu-item.color-light:hover {
+  color: white;
+  background-color: #f8f9fa;
+}
+.cru-menu-item.color-dark {
+  color: #212529;
+}
+.cru-menu-item.color-dark:hover {
+  color: white;
+  background-color: #212529;
+}
+
+.cru-menu-item-icon {
+  margin-right: 1em;
+}
+
+.cru-menu-divider {
+  border-top: 1px solid #e9ecef;
+}
+
+.cru-tab-pages-action-area {
+  display: flex;
+  align-items: center;
+}
+
+.cru-search-input {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.alert-container {
+  position: fixed;
+  z-index: 1070;
+}
+
+@media (min-width: 576px) {
+  .alert-container {
+    bottom: 0;
+    right: 0;
+  }
+}
+@media (max-width: 575.98px) {
+  .alert-container {
+    bottom: 0;
+    right: 0;
+    left: 0;
+    text-align: center;
+  }
+}
diff --git a/FrontEnd/src/views/home/home.sass b/FrontEnd/src/views/home/home.sass
deleted file mode 100644
index b4cda586..00000000
--- a/FrontEnd/src/views/home/home.sass
+++ /dev/null
@@ -1,29 +0,0 @@
-.home-timeline-list-item
-  display: flex
-  align-items: center
-
-.home-timeline-list-item-timeline
-  transition: background 0.8s
-  animation: 0.8s home-timeline-list-item-timeline-enter
-  &:hover
-    background: $gray-200
-
-@keyframes home-timeline-list-item-timeline-enter
-  from
-    transform: translate(-100%,0)
-    opacity: 0
-
-.home-timeline-list-item-line
-  width: 80px
-  flex-shrink: 0
-
-@keyframes home-timeline-list-loading-head-animation
-  from
-    transform: translate(0,-30px)
-    opacity: 1
-
-  to
-    opacity: 0
-
-.home-timeline-list-loading-head
-  animation: 1s infinite home-timeline-list-loading-head-animation
diff --git a/FrontEnd/src/views/home/index.css b/FrontEnd/src/views/home/index.css
new file mode 100644
index 00000000..516aba52
--- /dev/null
+++ b/FrontEnd/src/views/home/index.css
@@ -0,0 +1,73 @@
+.timeline-board {
+  min-height: 200px;
+  height: 100%;
+  position: relative;
+}
+
+.timeline-board-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.timeline-board-item {
+  font-size: 1.1em;
+  height: 48px;
+  transition: background 0.3s;
+  display: flex;
+  align-items: center;
+}
+.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;
+}
+
+.home-timeline-list-item {
+  display: flex;
+  align-items: center;
+}
+
+.home-timeline-list-item-timeline {
+  transition: background 0.8s;
+  animation: 0.8s home-timeline-list-item-timeline-enter;
+}
+.home-timeline-list-item-timeline:hover {
+  background: #e9ecef;
+}
+
+@keyframes home-timeline-list-item-timeline-enter {
+  from {
+    transform: translate(-100%, 0);
+    opacity: 0;
+  }
+}
+.home-timeline-list-item-line {
+  width: 80px;
+  flex-shrink: 0;
+}
+
+@keyframes home-timeline-list-loading-head-animation {
+  from {
+    transform: translate(0, -30px);
+    opacity: 1;
+  }
+  to {
+    opacity: 0;
+  }
+}
+.home-timeline-list-loading-head {
+  animation: 1s infinite home-timeline-list-loading-head-animation;
+}
diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx
index 0eca23ee..ddb72e76 100644
--- a/FrontEnd/src/views/home/index.tsx
+++ b/FrontEnd/src/views/home/index.tsx
@@ -8,6 +8,8 @@ import SearchInput from "../common/SearchInput";
 import TimelineListView from "./TimelineListView";
 import WebsiteIntroduction from "./WebsiteIntroduction";
 
+import "./index.css";
+
 const highlightTimelineMessageMap = {
   loading: "home.loadingHighlightTimelines",
   done: "home.loadedHighlightTimelines",
diff --git a/FrontEnd/src/views/login/index.css b/FrontEnd/src/views/login/index.css
new file mode 100644
index 00000000..dca7054d
--- /dev/null
+++ b/FrontEnd/src/views/login/index.css
@@ -0,0 +1,3 @@
+.login-container {
+  max-width: 600px;
+}
diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx
index 6adcef39..a39a9972 100644
--- a/FrontEnd/src/views/login/index.tsx
+++ b/FrontEnd/src/views/login/index.tsx
@@ -8,6 +8,8 @@ import { useUser, userService } from "@/services/user";
 import AppBar from "../common/AppBar";
 import LoadingButton from "../common/LoadingButton";
 
+import "./index.css";
+
 const LoginPage: React.FC = (_) => {
   const { t } = useTranslation();
   const history = useHistory();
diff --git a/FrontEnd/src/views/login/login.sass b/FrontEnd/src/views/login/login.sass
deleted file mode 100644
index 0bf385f5..00000000
--- a/FrontEnd/src/views/login/login.sass
+++ /dev/null
@@ -1,2 +0,0 @@
-.login-container
-  max-width: 600px
diff --git a/FrontEnd/src/views/search/index.css b/FrontEnd/src/views/search/index.css
new file mode 100644
index 00000000..6ff4d9fa
--- /dev/null
+++ b/FrontEnd/src/views/search/index.css
@@ -0,0 +1,15 @@
+.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
index 9a26802d..f5018c3e 100644
--- a/FrontEnd/src/views/search/index.tsx
+++ b/FrontEnd/src/views/search/index.tsx
@@ -11,6 +11,8 @@ 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 }) => {
diff --git a/FrontEnd/src/views/search/search.sass b/FrontEnd/src/views/search/search.sass
deleted file mode 100644
index 83f297fe..00000000
--- a/FrontEnd/src/views/search/search.sass
+++ /dev/null
@@ -1,13 +0,0 @@
-.timeline-search-result-item
-  @extend .rounded
-  border: 1px solid
-  border-color: $gray-200
-  background: $gray-100
-  transition: all 0.3s
-  &:hover
-    border-color: $primary
-    
-.timeline-search-result-item-avatar
-  width: 2em
-  height: 2em
-  border-radius: 50%
diff --git a/FrontEnd/src/views/settings/index.css b/FrontEnd/src/views/settings/index.css
new file mode 100644
index 00000000..566d501b
--- /dev/null
+++ b/FrontEnd/src/views/settings/index.css
@@ -0,0 +1,24 @@
+.change-avatar-cropper-row {
+  max-height: 400px;
+}
+
+.change-avatar-img {
+  min-width: 50%;
+  max-width: 100%;
+  max-height: 400px;
+}
+
+.settings-item {
+  padding: 0.5em 1em;
+  transition: background 0.3s;
+  border-bottom: 1px solid #e9ecef;
+}
+.settings-item.first {
+  border-top: 1px solid #e9ecef;
+}
+.settings-item.clickable {
+  cursor: pointer;
+}
+.settings-item:hover {
+  background: #dee2e6;
+}
diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx
index 04a2777a..f0bed222 100644
--- a/FrontEnd/src/views/settings/index.tsx
+++ b/FrontEnd/src/views/settings/index.tsx
@@ -9,6 +9,8 @@ import ChangePasswordDialog from "./ChangePasswordDialog";
 import ChangeAvatarDialog from "./ChangeAvatarDialog";
 import ChangeNicknameDialog from "./ChangeNicknameDialog";
 
+import "./index.css";
+
 const ConfirmLogoutDialog: React.FC<{
   onClose: () => void;
   onConfirm: () => void;
diff --git a/FrontEnd/src/views/settings/settings.sass b/FrontEnd/src/views/settings/settings.sass
deleted file mode 100644
index 8c6d24b8..00000000
--- a/FrontEnd/src/views/settings/settings.sass
+++ /dev/null
@@ -1,14 +0,0 @@
-.settings-item
-  padding: 0.5em 1em
-  transition: background 0.3s
-  border-bottom: 1px solid $gray-200
-
-  &.first
-    border-top: 1px solid $gray-200
-
-  &.clickable
-    cursor: pointer
-
-  &:hover
-    background: $gray-300
-
diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx
index 31ea5870..21daa5e2 100644
--- a/FrontEnd/src/views/timeline-common/Timeline.tsx
+++ b/FrontEnd/src/views/timeline-common/Timeline.tsx
@@ -1,5 +1,5 @@
 import React from "react";
-import { HubConnectionState } from "srcmicrosoft/signalr";
+import { HubConnectionState } from "@microsoft/signalr";
 
 import {
   HttpForbiddenError,
@@ -14,6 +14,8 @@ import TimelinePagedPostListView from "./TimelinePagedPostListView";
 import TimelineTop from "./TimelineTop";
 import TimelineLoading from "./TimelineLoading";
 
+import "./index.css";
+
 export interface TimelineProps {
   className?: string;
   style?: React.CSSProperties;
diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css
new file mode 100644
index 00000000..89399961
--- /dev/null
+++ b/FrontEnd/src/views/timeline-common/index.css
@@ -0,0 +1,289 @@
+.timeline {
+  z-index: 0;
+  position: relative;
+  width: 100%;
+  overflow-wrap: break-word;
+  animation: 1s timeline-enter;
+}
+
+@keyframes timeline-line-node-noncurrent {
+  to {
+    box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color);
+  }
+}
+@keyframes timeline-line-node-current {
+  to {
+    box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color);
+  }
+}
+@keyframes timeline-line-node-loading {
+  to {
+    box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color);
+  }
+}
+@keyframes timeline-line-node-loading-edge {
+  from {
+    transform: rotate(0turn);
+  }
+  to {
+    transform: rotate(1turn);
+  }
+}
+@keyframes timeline-enter {
+  from {
+    transform: translate(0, -100vh);
+  }
+}
+@keyframes timeline-top-loading-enter {
+  from {
+    transform: translate(0, -100%);
+  }
+}
+@keyframes timeline-post-enter {
+  from {
+    transform: translate(0, -100%);
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+.timeline-top-loading-enter {
+  animation: 1s timeline-top-loading-enter;
+}
+
+.timeline-line {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 30px;
+  position: absolute;
+  z-index: 1;
+  left: 2em;
+  top: 0;
+  bottom: 0;
+  transition: left 0.5s;
+}
+@media (max-width: 575.98px) {
+  .timeline-line {
+    left: 1em;
+  }
+}
+.timeline-line .segment {
+  width: 7px;
+  background: var(--tl-primary-color);
+}
+.timeline-line .segment.start {
+  height: 1.8em;
+  flex: 0 0 auto;
+}
+.timeline-line .segment.end {
+  flex: 1 1 auto;
+}
+.timeline-line .segment.current-end {
+  height: 2em;
+  flex: 0 0 auto;
+  background: linear-gradient(var(--tl-primary-enhance-color), white);
+}
+.timeline-line .node-container {
+  flex: 0 0 auto;
+  position: relative;
+  width: 18px;
+  height: 18px;
+}
+.timeline-line .node {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background: var(--tl-primary-color);
+  left: -1px;
+  top: -1px;
+  border-radius: 50%;
+  box-sizing: border-box;
+  z-index: 1;
+  animation: 1s infinite alternate;
+  animation-name: timeline-line-node-noncurrent;
+}
+.timeline-line .node-loading-edge {
+  color: var(--tl-primary-color);
+  width: 38px;
+  height: 38px;
+  position: absolute;
+  left: -10px;
+  top: -10px;
+  box-sizing: border-box;
+  z-index: 2;
+  animation: 1.5s linear infinite timeline-line-node-loading-edge;
+}
+.timeline-line.current .segment.start {
+  background: linear-gradient(
+    var(--tl-primary-color),
+    var(--tl-primary-enhance-color)
+  );
+}
+.timeline-line.current .segment.end {
+  background: var(--tl-primary-enhance-color);
+}
+.timeline-line.current .node {
+  background: var(--tl-primary-enhance-color);
+  animation-name: timeline-line-node-current;
+}
+.timeline-line.loading .node {
+  background: var(--tl-primary-color);
+  animation-name: timeline-line-node-loading;
+}
+
+.timeline-item.current {
+  padding-bottom: 2.5em;
+}
+
+.timeline-top {
+  position: relative;
+  text-align: right;
+}
+
+.timeline-item {
+  position: relative;
+  padding: 0.5em;
+}
+
+.timeline-item-card {
+  position: relative;
+  padding: 0.3em 0.5em 1em 4em;
+  transition: background 0.5s, padding-left 0.5s;
+  animation: 0.6s forwards;
+  opacity: 0;
+}
+@media (max-width: 575.98px) {
+  .timeline-item-card {
+    padding-left: 3em;
+  }
+}
+
+.timeline-item-header {
+  display: flex;
+  align-items: center;
+}
+
+.timeline-avatar {
+  border-radius: 50%;
+  width: 2em;
+  height: 2em;
+}
+
+.timeline-item-delete-button {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+}
+
+.timeline-content {
+  white-space: pre-line;
+}
+
+.timeline-content-image {
+  max-width: 80%;
+  max-height: 200px;
+}
+
+.timeline-date-item {
+  position: relative;
+  padding: 0.3em 0 0.3em 4em;
+}
+
+.timeline-date-item-badge {
+  display: inline-block;
+  padding: 0.1em 0.4em;
+  border-radius: 0.4em;
+  background: #7c7c7c;
+  color: white;
+  font-size: 0.8em;
+}
+
+.timeline-post-edit-image {
+  max-width: 100px;
+  max-height: 100px;
+}
+
+.mask {
+  background: rgba(255, 255, 255, 0.8);
+  z-index: 100;
+}
+
+.timeline-sync-state-badge {
+  font-size: 0.8em;
+  padding: 3px 8px;
+  border-radius: 5px;
+  background: #e8fbff;
+}
+
+.timeline-sync-state-badge-pin {
+  display: inline-block;
+  width: 0.4em;
+  height: 0.4em;
+  border-radius: 50%;
+  vertical-align: middle;
+  margin-right: 0.6em;
+}
+
+.timeline-template-card {
+  position: fixed;
+  top: 56px;
+  right: 0;
+  margin: 0.5em;
+}
+
+.timeline-markdown-post-edit-page {
+  overflow: scroll;
+  max-height: 300px;
+}
+
+.timeline-markdown-post-edit-image-container {
+  position: relative;
+  text-align: center;
+  margin-bottom: 1em;
+}
+
+.timeline-markdown-post-edit-image {
+  max-width: 100%;
+  max-height: 200px;
+}
+
+.timeline-markdown-post-edit-image-delete-button {
+  position: absolute;
+  right: 10px;
+  top: 2px;
+}
+
+.connection-status-badge {
+  font-size: 0.8em;
+  border-radius: 5px;
+  padding: 0.1em 1em;
+  background-color: #eaf2ff;
+}
+.connection-status-badge::before {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  display: inline-block;
+  content: "";
+  margin-right: 0.6em;
+}
+.connection-status-badge.success {
+  color: #006100;
+}
+.connection-status-badge.success::before {
+  background-color: #006100;
+}
+.connection-status-badge.warning {
+  color: #e4a700;
+}
+.connection-status-badge.warning::before {
+  background-color: #e4a700;
+}
+.connection-status-badge.danger {
+  color: #fd1616;
+}
+.connection-status-badge.danger::before {
+  background-color: #fd1616;
+}
diff --git a/FrontEnd/src/views/timeline-common/timeline-common.sass b/FrontEnd/src/views/timeline-common/timeline-common.sass
deleted file mode 100644
index 4400fead..00000000
--- a/FrontEnd/src/views/timeline-common/timeline-common.sass
+++ /dev/null
@@ -1,259 +0,0 @@
-@use 'sass:color'
-
-.timeline
-  z-index: 0
-  position: relative
-  width: 100%
-  overflow-wrap: break-word
-  animation: 1s timeline-enter
-
-$timeline-line-width: 7px
-$timeline-line-node-radius: 18px
-$timeline-line-color: var(--tl-primary-color)
-$timeline-line-color-current: var(--tl-primary-enhance-color)
-
-@keyframes timeline-line-node-noncurrent
-  to
-    box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color)
-
-@keyframes timeline-line-node-current
-  to
-    box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color)
-
-@keyframes timeline-line-node-loading
-  to
-    box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color)
-
-@keyframes timeline-line-node-loading-edge
-  from
-    transform: rotate(0turn)
-  to
-    transform: rotate(1turn)
-
-@keyframes timeline-enter
-  from
-    transform: translate(0, -100vh)
-
-@keyframes timeline-top-loading-enter
-  from
-    transform: translate(0, -100%)
-
-@keyframes timeline-post-enter
-  from
-    transform: translate(0, -100%)
-    opacity: 0
-
-  to
-    opacity: 1
-
-.timeline-top-loading-enter
-  animation: 1s timeline-top-loading-enter
-
-.timeline-line
-  display: flex
-  flex-direction: column
-  align-items: center
-  width: 30px
-
-  position: absolute
-  z-index: 1
-  left: 2em
-  top: 0
-  bottom: 0
-
-  transition: left 0.5s
-
-  @include media-breakpoint-down(sm)
-    left: 1em
-
-  .segment
-    width: $timeline-line-width
-    background: $timeline-line-color
-
-    &.start
-      height: 1.8em
-      flex: 0 0 auto
-
-    &.end
-      flex: 1 1 auto
-
-    &.current-end
-      height: 2em
-      flex: 0 0 auto
-      background: linear-gradient($timeline-line-color-current, white)
-
-  .node-container
-    flex: 0 0 auto
-    position: relative
-    width: $timeline-line-node-radius
-    height: $timeline-line-node-radius
-
-  .node
-    width: $timeline-line-node-radius + 2
-    height: $timeline-line-node-radius + 2
-    position: absolute
-    background: $timeline-line-color
-    left: -1px
-    top: -1px
-    border-radius: 50%
-    box-sizing: border-box
-    z-index: 1
-    animation: 1s infinite alternate
-    animation-name: timeline-line-node-noncurrent
-
-  .node-loading-edge
-    color: $timeline-line-color
-    width: $timeline-line-node-radius + 20
-    height: $timeline-line-node-radius + 20
-    position: absolute
-    left: -10px
-    top: -10px
-    box-sizing: border-box
-    z-index: 2
-    animation: 1.5s linear infinite timeline-line-node-loading-edge
-
-  &.current
-    .segment
-      &.start
-        background: linear-gradient($timeline-line-color, $timeline-line-color-current)
-      &.end
-        background: $timeline-line-color-current
-    .node
-      background: $timeline-line-color-current
-      animation-name: timeline-line-node-current
-
-  &.loading
-    .node
-      background: $timeline-line-color
-      animation-name: timeline-line-node-loading
-
-.timeline-item.current
-  padding-bottom: 2.5em
-
-.timeline-top
-  position: relative
-  text-align: right
-
-.timeline-item
-  position: relative
-  padding: 0.5em
-
-.timeline-item-card
-  @extend .cru-card
-  position: relative
-  padding: 0.3em 0.5em 1em 4em
-  transition: background 0.5s, padding-left 0.5s
-  animation: 0.6s forwards
-  opacity: 0
-
-  @include media-breakpoint-down(sm)
-    padding-left: 3em
-
-.timeline-item-header
-  display: flex
-  align-items: center
-  @extend .my-2
-
-.timeline-avatar
-  border-radius: 50%
-  width: 2em
-  height: 2em
-
-.timeline-item-delete-button
-  position: absolute
-  right: 0
-  bottom: 0
-
-.timeline-content
-  white-space: pre-line
-
-.timeline-content-image
-  max-width: 80%
-  max-height: 200px
-
-.timeline-date-item
-  position: relative
-  padding: 0.3em 0 0.3em 4em
-
-.timeline-date-item-badge
-  display: inline-block
-  padding: 0.1em 0.4em
-  border-radius: 0.4em
-  background: #7c7c7c
-  color: white
-  font-size: 0.8em
-
-.timeline-post-edit-image
-  max-width: 100px
-  max-height: 100px
-
-.mask
-  background: change-color($color: white, $alpha: 0.8)
-  z-index: 100
-
-.timeline-sync-state-badge
-  font-size: 0.8em
-  padding: 3px 8px
-  border-radius: 5px
-  background: #e8fbff
-
-.timeline-sync-state-badge-pin
-  display: inline-block
-  width: 0.4em
-  height: 0.4em
-  border-radius: 50%
-  vertical-align: middle
-  margin-right: 0.6em
-
-.timeline-template-card
-  position: fixed
-  top: 56px
-  right: 0
-  margin: 0.5em
-
-.timeline-markdown-post-edit-page
-  overflow: scroll
-  max-height: 300px
-
-.timeline-markdown-post-edit-image-container
-  position: relative
-  text-align: center
-  margin-bottom: 1em
-
-.timeline-markdown-post-edit-image
-  max-width: 100%
-  max-height: 200px
-
-.timeline-markdown-post-edit-image-delete-button
-  position: absolute
-  right: 10px
-  top: 2px
-
-.connection-status-badge
-  font-size: 0.8em
-  border-radius: 5px
-  padding: 0.1em 1em
-  background-color: rgb(234 242 255)
-
-  &::before
-    width: 10px
-    height: 10px
-    border-radius: 50%
-    display: inline-block
-    content: ''
-    margin-right: 0.6em
-
-  &.success
-    color: #006100
-    &::before
-      background-color: #006100
-
-  &.warning
-    color: #e4a700
-    &::before
-      background-color: #e4a700
-
-  &.danger
-    color: #fd1616
-    &::before
-      background-color: #fd1616
diff --git a/FrontEnd/src/views/timeline/timeline.sass b/FrontEnd/src/views/timeline/timeline.sass
deleted file mode 100644
index e69de29b..00000000
diff --git a/FrontEnd/src/views/user/index.css b/FrontEnd/src/views/user/index.css
new file mode 100644
index 00000000..35f01d38
--- /dev/null
+++ b/FrontEnd/src/views/user/index.css
@@ -0,0 +1,9 @@
+.change-avatar-cropper-row {
+  max-height: 400px;
+}
+
+.change-avatar-img {
+  min-width: 50%;
+  max-width: 100%;
+  max-height: 400px;
+}
diff --git a/FrontEnd/src/views/user/index.tsx b/FrontEnd/src/views/user/index.tsx
index 0013b254..1f2fe9ed 100644
--- a/FrontEnd/src/views/user/index.tsx
+++ b/FrontEnd/src/views/user/index.tsx
@@ -4,6 +4,8 @@ import { useParams } from "react-router";
 import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate";
 import UserCard from "./UserCard";
 
+import "./index.css";
+
 const UserPage: React.FC = () => {
   const { username } = useParams<{ username: string }>();
 
diff --git a/FrontEnd/src/views/user/user.sass b/FrontEnd/src/views/user/user.sass
deleted file mode 100644
index 63a28e05..00000000
--- a/FrontEnd/src/views/user/user.sass
+++ /dev/null
@@ -1,7 +0,0 @@
-.change-avatar-cropper-row
-  max-height: 400px
-
-.change-avatar-img
-  min-width: 50%
-  max-width: 100%
-  max-height: 400px
-- 
cgit v1.2.3

