From 538d6830a0022b49b99695095d85e567b0c86e71 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sun, 30 Jul 2023 23:47:53 +0800
Subject: ...

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

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

diff --git a/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx b/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx
new file mode 100644
index 00000000..5f4ac706
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+import TimelineLine from "./TimelineLine";
+
+export interface TimelineDateItemProps {
+  date: Date;
+}
+
+const TimelineDateLabel: React.FC<TimelineDateItemProps> = ({ date }) => {
+  return (
+    <div className="timeline-date-item">
+      <TimelineLine center="none" />
+      <div className="timeline-date-item-badge">
+        {date.toLocaleDateString()}
+      </div>
+    </div>
+  );
+};
+
+export default TimelineDateLabel;
-- 
cgit v1.2.3


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

---
 FrontEnd/package.json                              |  2 +-
 FrontEnd/pnpm-lock.yaml                            | 35 +++++++-
 FrontEnd/src/pages/timeline/Timeline.css           | 95 +++++++---------------
 FrontEnd/src/pages/timeline/Timeline.tsx           | 77 ++++++------------
 FrontEnd/src/pages/timeline/TimelineCard.tsx       |  2 +-
 FrontEnd/src/pages/timeline/TimelineDateLabel.css  |  9 ++
 FrontEnd/src/pages/timeline/TimelineDateLabel.tsx  | 20 ++---
 FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx  | 25 ------
 FrontEnd/src/pages/timeline/TimelineLine.tsx       | 51 ------------
 FrontEnd/src/pages/timeline/TimelineLoading.tsx    | 16 ----
 FrontEnd/src/pages/timeline/TimelinePostCard.css   |  9 ++
 FrontEnd/src/pages/timeline/TimelinePostCard.tsx   | 22 +++++
 .../src/pages/timeline/TimelinePostContainer.css   |  3 +
 .../src/pages/timeline/TimelinePostContainer.tsx   | 20 +++++
 FrontEnd/src/pages/timeline/TimelinePostEdit.tsx   |  6 +-
 .../src/pages/timeline/TimelinePostEditCard.tsx    | 31 -------
 .../src/pages/timeline/TimelinePostEditNoLogin.tsx | 18 ----
 FrontEnd/src/pages/timeline/TimelinePostList.css   | 10 +++
 FrontEnd/src/pages/timeline/TimelinePostList.tsx   | 76 +++++++++++++++++
 .../src/pages/timeline/TimelinePostListView.tsx    | 76 -----------------
 FrontEnd/src/pages/timeline/TimelinePostView.css   | 23 +++---
 FrontEnd/src/pages/timeline/TimelinePostView.tsx   | 20 ++---
 22 files changed, 265 insertions(+), 381 deletions(-)
 create mode 100644 FrontEnd/src/pages/timeline/TimelineDateLabel.css
 delete mode 100644 FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx
 delete mode 100644 FrontEnd/src/pages/timeline/TimelineLine.tsx
 delete mode 100644 FrontEnd/src/pages/timeline/TimelineLoading.tsx
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostCard.css
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostCard.tsx
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostContainer.css
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostContainer.tsx
 delete mode 100644 FrontEnd/src/pages/timeline/TimelinePostEditCard.tsx
 delete mode 100644 FrontEnd/src/pages/timeline/TimelinePostEditNoLogin.tsx
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostList.css
 create mode 100644 FrontEnd/src/pages/timeline/TimelinePostList.tsx
 delete mode 100644 FrontEnd/src/pages/timeline/TimelinePostListView.tsx

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

diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index 56af818c..950670f4 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -12,8 +12,8 @@
     "check:fix": "pnpm run type-check && pnpm run lint:fix"
   },
   "dependencies": {
+    "@floating-ui/react-dom": "^2.0.1",
     "@microsoft/signalr": "^7.0.7",
-    "@popperjs/core": "^2.11.8",
     "axios": "^1.4.0",
     "bootstrap": "^5.3.0",
     "bootstrap-icons": "^1.10.5",
diff --git a/FrontEnd/pnpm-lock.yaml b/FrontEnd/pnpm-lock.yaml
index cad9a287..24e81c7c 100644
--- a/FrontEnd/pnpm-lock.yaml
+++ b/FrontEnd/pnpm-lock.yaml
@@ -5,12 +5,12 @@ settings:
   excludeLinksFromLockfile: false
 
 dependencies:
+  '@floating-ui/react-dom':
+    specifier: ^2.0.1
+    version: 2.0.1(react-dom@18.2.0)(react@18.2.0)
   '@microsoft/signalr':
     specifier: ^7.0.7
     version: 7.0.7
-  '@popperjs/core':
-    specifier: ^2.11.8
-    version: 2.11.8
   axios:
     specifier: ^1.4.0
     version: 1.4.0
@@ -239,6 +239,34 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
+  /@floating-ui/core@1.4.1:
+    resolution: {integrity: sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==}
+    dependencies:
+      '@floating-ui/utils': 0.1.1
+    dev: false
+
+  /@floating-ui/dom@1.5.1:
+    resolution: {integrity: sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==}
+    dependencies:
+      '@floating-ui/core': 1.4.1
+      '@floating-ui/utils': 0.1.1
+    dev: false
+
+  /@floating-ui/react-dom@2.0.1(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+    dependencies:
+      '@floating-ui/dom': 1.5.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /@floating-ui/utils@0.1.1:
+    resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
+    dev: false
+
   /@humanwhocodes/config-array@0.11.10:
     resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
     engines: {node: '>=10.10.0'}
@@ -3147,6 +3175,7 @@ packages:
   /node-gyp-build-optional-packages@5.0.7:
     resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==}
     hasBin: true
+    requiresBuild: true
     dev: true
     optional: true
 
diff --git a/FrontEnd/src/pages/timeline/Timeline.css b/FrontEnd/src/pages/timeline/Timeline.css
index f071f163..30ad75c9 100644
--- a/FrontEnd/src/pages/timeline/Timeline.css
+++ b/FrontEnd/src/pages/timeline/Timeline.css
@@ -1,7 +1,19 @@
+.timeline {
+  --timeline-background-color: #f3f3f3;
+  --timeline-shadow-color: #00000080;
+  --timeline-post-line-color: #eadd2c;
+  --timeline-post-line-shadow: 2px 1px 10px -1px var(--timeline-shadow-color);
+  --timeline-post-card-background-color: rgb(255, 255, 255);
+  --timeline-post-card-shadow: 4px 2px 10px -2px var(--timeline-shadow-color);
+  --timeline-post-card-border-radius: 10px;
+  --timeline-post-text-color: #000000;
+}
+
 .timeline {
   z-index: 0;
   position: relative;
   width: 100%;
+  background-color: var(--timeline-background-color);
 }
 
 @keyframes timeline-line-node {
@@ -26,6 +38,7 @@
   from {
     transform: rotate(0turn);
   }
+
   to {
     transform: rotate(1turn);
   }
@@ -42,6 +55,7 @@
     transform: translate(0, 100%);
     opacity: 0;
   }
+
   to {
     opacity: 1;
   }
@@ -71,40 +85,46 @@
 }
 
 .timeline-line .segment {
-  width: 7px;
+  width: 12px;
   background: var(--cru-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(--cru-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;
+  width: 24px;
+  height: 24px;
   position: absolute;
   background: var(--cru-primary-color);
-  left: -1px;
-  top: -1px;
+  left: -3px;
+  top: -3px;
   border-radius: 50%;
   box-sizing: border-box;
   z-index: 1;
   animation: 1s infinite alternate;
   animation-name: timeline-line-node;
 }
+
 .timeline-line .node-loading-edge {
   color: var(--cru-primary-color);
   width: 38px;
@@ -116,11 +136,10 @@
   z-index: 2;
   animation: 1.5s linear infinite timeline-line-node-loading-edge;
 }
+
 .timeline-line.current .segment.start {
-  background: linear-gradient(
-    var(--cru-primary-color),
-    var(--cru-primary-enhance-color)
-  );
+  background: linear-gradient(var(--cru-primary-color),
+      var(--cru-primary-enhance-color));
 }
 
 .timeline-line.current .segment.end {
@@ -137,67 +156,11 @@
   animation-name: timeline-line-node-loading;
 }
 
-.timeline-item {
-  position: relative;
-  padding: 0.5em;
-}
-
-.timeline-item-card {
-  position: relative;
-  padding: 0.5em 0.5em 0.5em 4em;
-}
-
-.timeline-item-card.enter-animation {
-  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-item-options-mask {
   background: rgba(255, 255, 255, 0.85);
   z-index: 100;
@@ -234,4 +197,4 @@
 .timeline-top {
   position: sticky;
   top: 56px;
-}
+}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx
index f93e1623..317d602e 100644
--- a/FrontEnd/src/pages/timeline/Timeline.tsx
+++ b/FrontEnd/src/pages/timeline/Timeline.tsx
@@ -1,4 +1,4 @@
-import * as React from "react";
+import { useState, useEffect } from "react";
 import classnames from "classnames";
 import { useScrollToBottom } from "@/utilities/hooks";
 import { HubConnectionState } from "@microsoft/signalr";
@@ -14,56 +14,49 @@ import {
   HttpTimelinePostInfo,
 } from "@/http/timeline";
 
-import { useUser } from "@/services/user";
 import { getTimelinePostUpdate$ } from "@/services/timeline";
 
-import TimelinePostListView from "./TimelinePostListView";
-import TimelineEmptyItem from "./TimelineEmptyItem";
-import TimelineLoading from "./TimelineLoading";
+import TimelinePostList from "./TimelinePostList";
 import TimelinePostEdit from "./TimelinePostEdit";
-import TimelinePostEditNoLogin from "./TimelinePostEditNoLogin";
 import TimelineCard from "./TimelineCard";
 
 import "./Timeline.css";
 
 export interface TimelineProps {
   className?: string;
-  style?: React.CSSProperties;
   timelineOwner: string;
   timelineName: string;
 }
 
-const Timeline: React.FC<TimelineProps> = (props) => {
-  const { timelineOwner, timelineName, className, style } = props;
+export function Timeline(props: TimelineProps) {
+  const { timelineOwner, timelineName, className } = props;
 
-  const user = useUser();
-
-  const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);
-  const [posts, setPosts] = React.useState<HttpTimelinePostInfo[] | null>(null);
-  const [signalrState, setSignalrState] = React.useState<HubConnectionState>(
+  const [timeline, setTimeline] = useState<HttpTimelineInfo | null>(null);
+  const [posts, setPosts] = useState<HttpTimelinePostInfo[] | null>(null);
+  const [signalrState, setSignalrState] = useState<HubConnectionState>(
     HubConnectionState.Connecting,
   );
-  const [error, setError] = React.useState<
+  const [error, setError] = useState<
     "offline" | "forbid" | "notfound" | "error" | null
   >(null);
 
-  const [currentPage, setCurrentPage] = React.useState(1);
-  const [totalPage, setTotalPage] = React.useState(0);
+  const [currentPage, setCurrentPage] = useState(1);
+  const [totalPage, setTotalPage] = useState(0);
 
-  const [timelineReloadKey, setTimelineReloadKey] = React.useState(0);
-  const [postsReloadKey, setPostsReloadKey] = React.useState(0);
+  const [timelineReloadKey, setTimelineReloadKey] = useState(0);
+  const [postsReloadKey, setPostsReloadKey] = useState(0);
 
   const updateTimeline = (): void => setTimelineReloadKey((o) => o + 1);
   const updatePosts = (): void => setPostsReloadKey((o) => o + 1);
 
-  React.useEffect(() => {
+  useEffect(() => {
     setTimeline(null);
     setPosts(null);
     setError(null);
     setSignalrState(HubConnectionState.Connecting);
   }, [timelineOwner, timelineName]);
 
-  React.useEffect(() => {
+  useEffect(() => {
     getHttpTimelineClient()
       .getTimeline(timelineOwner, timelineName)
       .then(
@@ -85,7 +78,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
       );
   }, [timelineOwner, timelineName, timelineReloadKey]);
 
-  React.useEffect(() => {
+  useEffect(() => {
     getHttpTimelineClient()
       .listPost(timelineOwner, timelineName, 1)
       .then(
@@ -110,7 +103,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
       );
   }, [timelineOwner, timelineName, postsReloadKey]);
 
-  React.useEffect(() => {
+  useEffect(() => {
     const timelinePostUpdate$ = getTimelinePostUpdate$(
       timelineOwner,
       timelineName,
@@ -154,33 +147,16 @@ const Timeline: React.FC<TimelineProps> = (props) => {
   }, currentPage < totalPage);
 
   if (error === "offline") {
-    return (
-      <div className={className} style={style}>
-        Offline.
-      </div>
-    );
+    return <div className={className}>Offline.</div>;
   } else if (error === "notfound") {
-    return (
-      <div className={className} style={style}>
-        Not exist.
-      </div>
-    );
+    return <div className={className}>Not exist.</div>;
   } else if (error === "forbid") {
-    return (
-      <div className={className} style={style}>
-        Forbid.
-      </div>
-    );
+    return <div className={className}>Forbid.</div>;
   } else if (error === "error") {
-    return (
-      <div className={className} style={style}>
-        Error.
-      </div>
-    );
+    return <div className={className}>Error.</div>;
   }
   return (
     <>
-      {timeline == null && posts == null && <TimelineLoading />}
       {timeline && (
         <TimelineCard
           timeline={timeline}
@@ -189,18 +165,15 @@ const Timeline: React.FC<TimelineProps> = (props) => {
         />
       )}
       {posts && (
-        <div style={style} className={classnames("timeline", className)}>
-          <TimelineEmptyItem className="timeline-top" height={50} />
-          {timeline?.postable ? (
+        <div className={classnames("timeline", className)}>
+          {timeline?.postable && (
             <TimelinePostEdit timeline={timeline} onPosted={updatePosts} />
-          ) : user == null ? (
-            <TimelinePostEditNoLogin />
-          ) : null}
-          <TimelinePostListView posts={posts} onReload={updatePosts} />
+          )}
+          <TimelinePostList posts={posts} onReload={updatePosts} />
         </div>
       )}
     </>
   );
-};
+}
 
 export default Timeline;
diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx
index b287c620..04b34ec1 100644
--- a/FrontEnd/src/pages/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx
@@ -123,7 +123,7 @@ export default function TimelineCard(props: TimelinePageCardProps) {
 
   return (
     <Card
-      color="secondary"
+      color="tertiary"
       className={`timeline-card timeline-card-${
         collapse ? "collapse" : "expand"
       }`}
diff --git a/FrontEnd/src/pages/timeline/TimelineDateLabel.css b/FrontEnd/src/pages/timeline/TimelineDateLabel.css
new file mode 100644
index 00000000..1c66045b
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelineDateLabel.css
@@ -0,0 +1,9 @@
+.timeline-post-date-badge {
+  display: inline-block;
+  padding: 0.1em 0.4em;
+  border-radius: 0.4em;
+  background: #7c7c7c;
+  color: white;
+  font-size: 0.8em;
+  margin-left: 5em;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx b/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx
index 5f4ac706..eaadcc1a 100644
--- a/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx
+++ b/FrontEnd/src/pages/timeline/TimelineDateLabel.tsx
@@ -1,19 +1,13 @@
-import * as React from "react";
-import TimelineLine from "./TimelineLine";
+import TimelinePostContainer from "./TimelinePostContainer";
 
-export interface TimelineDateItemProps {
-  date: Date;
-}
+import "./TimelineDateLabel.css";
 
-const TimelineDateLabel: React.FC<TimelineDateItemProps> = ({ date }) => {
+export default function TimelineDateLabel({ date }: { date: Date }) {
   return (
-    <div className="timeline-date-item">
-      <TimelineLine center="none" />
-      <div className="timeline-date-item-badge">
+    <TimelinePostContainer>
+      <div className="timeline-post-date-badge">
         {date.toLocaleDateString()}
       </div>
-    </div>
+    </TimelinePostContainer>
   );
-};
-
-export default TimelineDateLabel;
+}
diff --git a/FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx b/FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx
deleted file mode 100644
index 5e0728d4..00000000
--- a/FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import TimelineLine, { TimelineLineProps } from "./TimelineLine";
-
-export interface TimelineEmptyItemProps extends Partial<TimelineLineProps> {
-  height?: number | string;
-  className?: string;
-  style?: React.CSSProperties;
-}
-
-const TimelineEmptyItem: React.FC<TimelineEmptyItemProps> = (props) => {
-  const { height, style, className, center, ...lineProps } = props;
-
-  return (
-    <div
-      style={{ ...style, height: height }}
-      className={classnames("timeline-item", className)}
-    >
-      <TimelineLine center={center ?? "none"} {...lineProps} />
-    </div>
-  );
-};
-
-export default TimelineEmptyItem;
diff --git a/FrontEnd/src/pages/timeline/TimelineLine.tsx b/FrontEnd/src/pages/timeline/TimelineLine.tsx
deleted file mode 100644
index 4a87e6e0..00000000
--- a/FrontEnd/src/pages/timeline/TimelineLine.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-export interface TimelineLineProps {
-  current?: boolean;
-  startSegmentLength?: string | number;
-  center: "node" | "loading" | "none";
-  className?: string;
-  style?: React.CSSProperties;
-}
-
-const TimelineLine: React.FC<TimelineLineProps> = ({
-  startSegmentLength,
-  center,
-  current,
-  className,
-  style,
-}) => {
-  return (
-    <div
-      className={classnames(
-        "timeline-line",
-        current && "current",
-        center === "loading" && "loading",
-        className
-      )}
-      style={style}
-    >
-      <div className="segment start" style={{ height: startSegmentLength }} />
-      {center !== "none" ? (
-        <div className="node-container">
-          <div className="node"></div>
-          {center === "loading" ? (
-            <svg className="node-loading-edge" viewBox="0 0 100 100">
-              <path
-                d="M 50,10 A 40 40 45 0 1 78.28,21.72"
-                stroke="currentcolor"
-                strokeLinecap="square"
-                strokeWidth="8"
-              />
-            </svg>
-          ) : null}
-        </div>
-      ) : null}
-      {center !== "loading" ? <div className="segment end"></div> : null}
-      {current && <div className="segment current-end" />}
-    </div>
-  );
-};
-
-export default TimelineLine;
diff --git a/FrontEnd/src/pages/timeline/TimelineLoading.tsx b/FrontEnd/src/pages/timeline/TimelineLoading.tsx
deleted file mode 100644
index f876cba9..00000000
--- a/FrontEnd/src/pages/timeline/TimelineLoading.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as React from "react";
-
-import TimelineEmptyItem from "./TimelineEmptyItem";
-
-const TimelineLoading: React.FC = () => {
-  return (
-    <TimelineEmptyItem
-      className="timeline-top-loading-enter"
-      height={100}
-      center="loading"
-      startSegmentLength={56}
-    />
-  );
-};
-
-export default TimelineLoading;
diff --git a/FrontEnd/src/pages/timeline/TimelinePostCard.css b/FrontEnd/src/pages/timeline/TimelinePostCard.css
new file mode 100644
index 00000000..3a446f44
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostCard.css
@@ -0,0 +1,9 @@
+.timeline-post-card {
+  padding: 1em 1em 1em 3em;
+  color: var(--timeline-post-text-color);
+  background-color: var(--timeline-post-card-background-color);
+  box-shadow: var(--timeline-post-card-shadow);
+  border-radius: var(--timeline-post-card-border-radius);
+  position: relative;
+  z-index: 1;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx
new file mode 100644
index 00000000..83479349
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx
@@ -0,0 +1,22 @@
+import { ReactNode } from "react";
+import classNames from "classnames";
+
+import Card from "@/views/common/Card";
+
+import "./TimelinePostCard.css";
+
+export interface TimelinePostEditCardProps {
+  className?: string;
+  children?: ReactNode;
+}
+
+export default function TimelinePostCard({
+  className,
+  children,
+}: TimelinePostEditCardProps) {
+  return (
+    <Card className={classNames("timeline-post-card", className)}>
+      {children}
+    </Card>
+  );
+}
diff --git a/FrontEnd/src/pages/timeline/TimelinePostContainer.css b/FrontEnd/src/pages/timeline/TimelinePostContainer.css
new file mode 100644
index 00000000..a12f70b1
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostContainer.css
@@ -0,0 +1,3 @@
+.timeline-post-container {
+  padding: 0.5em 1em;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx b/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx
new file mode 100644
index 00000000..4697268b
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx
@@ -0,0 +1,20 @@
+import { ReactNode } from "react";
+import classNames from "classnames";
+
+import "./TimelinePostContainer.css";
+
+export interface TimelinePostEditCardProps {
+  className?: string;
+  children?: ReactNode;
+}
+
+export default function TimelinePostContainer({
+  className,
+  children,
+}: TimelinePostEditCardProps) {
+  return (
+    <div className={classNames("timeline-post-container", className)}>
+      {children}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx b/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx
index c1fa0dd9..b0cc763a 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx
@@ -18,7 +18,7 @@ import BlobImage from "@/views/common/BlobImage";
 import LoadingButton from "@/views/common/button/LoadingButton";
 import PopupMenu from "@/views/common/menu/PopupMenu";
 import MarkdownPostEdit from "./MarkdownPostEdit";
-import TimelinePostEditCard from "./TimelinePostEditCard";
+import TimelinePostEditCard from "./TimelinePostContainer";
 import IconButton from "@/views/common/button/IconButton";
 
 import "./TimelinePostEdit.css";
@@ -118,7 +118,7 @@ export interface TimelinePostEditProps {
 }
 
 const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
-  const { timeline, style, className, onPosted } = props;
+  const { timeline, className, onPosted } = props;
 
   const { t } = useTranslation();
 
@@ -195,7 +195,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
   };
 
   return (
-    <TimelinePostEditCard className={className} style={style}>
+    <TimelinePostEditCard className={className}>
       {showMarkdown ? (
         <MarkdownPostEdit
           className="cru-fill-parent"
diff --git a/FrontEnd/src/pages/timeline/TimelinePostEditCard.tsx b/FrontEnd/src/pages/timeline/TimelinePostEditCard.tsx
deleted file mode 100644
index fdd53983..00000000
--- a/FrontEnd/src/pages/timeline/TimelinePostEditCard.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import Card from "@/views/common/Card";
-import TimelineLine from "./TimelineLine";
-
-import "./TimelinePostEdit.css";
-
-export interface TimelinePostEditCardProps {
-  className?: string;
-  style?: React.CSSProperties;
-  children?: React.ReactNode;
-}
-
-const TimelinePostEdit: React.FC<TimelinePostEditCardProps> = ({
-  className,
-  style,
-  children,
-}) => {
-  return (
-    <div
-      className={classnames("timeline-item timeline-post-edit", className)}
-      style={style}
-    >
-      <TimelineLine center="node" />
-      <Card className="timeline-item-card">{children}</Card>
-    </div>
-  );
-};
-
-export default TimelinePostEdit;
diff --git a/FrontEnd/src/pages/timeline/TimelinePostEditNoLogin.tsx b/FrontEnd/src/pages/timeline/TimelinePostEditNoLogin.tsx
deleted file mode 100644
index 1ef0a287..00000000
--- a/FrontEnd/src/pages/timeline/TimelinePostEditNoLogin.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as React from "react";
-import { Trans } from "react-i18next";
-import { Link } from "react-router-dom";
-
-import TimelinePostEditCard from "./TimelinePostEditCard";
-
-export default function TimelinePostEditNoLogin(): React.ReactElement | null {
-  return (
-    <TimelinePostEditCard>
-      <div className="mt-3 mb-4">
-        <Trans
-          i18nKey="timeline.postNoLogin"
-          components={{ l: <Link to="/login" /> }}
-        />
-      </div>
-    </TimelinePostEditCard>
-  );
-}
diff --git a/FrontEnd/src/pages/timeline/TimelinePostList.css b/FrontEnd/src/pages/timeline/TimelinePostList.css
new file mode 100644
index 00000000..bd575554
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostList.css
@@ -0,0 +1,10 @@
+.timeline-post-timeline {
+  position: absolute;
+  left: 2.5em;
+  width: 1em;
+  top: 0;
+  bottom: 0;
+  background-color: var(--timeline-post-line-color);
+  box-shadow: var(--timeline-post-line-shadow);
+  z-index: -1;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/pages/timeline/TimelinePostList.tsx b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
new file mode 100644
index 00000000..a3501b33
--- /dev/null
+++ b/FrontEnd/src/pages/timeline/TimelinePostList.tsx
@@ -0,0 +1,76 @@
+import { useMemo, Fragment } from "react";
+
+import { HttpTimelinePostInfo } from "@/http/timeline";
+
+import TimelinePostView from "./TimelinePostView";
+import TimelineDateLabel from "./TimelineDateLabel";
+
+import "./TimelinePostList.css";
+
+function dateEqual(left: Date, right: Date): boolean {
+  return (
+    left.getDate() == right.getDate() &&
+    left.getMonth() == right.getMonth() &&
+    left.getFullYear() == right.getFullYear()
+  );
+}
+
+interface TimelinePostListViewProps {
+  posts: HttpTimelinePostInfo[];
+  onReload: () => void;
+}
+
+export default function TimelinePostList(props: TimelinePostListViewProps) {
+  const { posts, onReload } = props;
+
+  const groupedPosts = useMemo<
+    {
+      date: Date;
+      posts: (HttpTimelinePostInfo & { index: number })[];
+    }[]
+  >(() => {
+    const result: {
+      date: Date;
+      posts: (HttpTimelinePostInfo & { index: number })[];
+    }[] = [];
+    let index = 0;
+    for (const post of posts) {
+      const time = new Date(post.time);
+      if (result.length === 0) {
+        result.push({ date: time, posts: [{ ...post, index }] });
+      } else {
+        const lastGroup = result[result.length - 1];
+        if (dateEqual(lastGroup.date, time)) {
+          lastGroup.posts.push({ ...post, index });
+        } else {
+          result.push({ date: time, posts: [{ ...post, index }] });
+        }
+      }
+      index++;
+    }
+    return result;
+  }, [posts]);
+
+  return (
+    <div>
+      <div className="timeline-post-timeline" />
+      {groupedPosts.map((group) => {
+        return (
+          <Fragment key={group.date.toDateString()}>
+            <TimelineDateLabel date={group.date} />
+            {group.posts.map((post) => {
+              return (
+                <TimelinePostView
+                  key={post.id}
+                  post={post}
+                  onChanged={onReload}
+                  onDeleted={onReload}
+                />
+              );
+            })}
+          </Fragment>
+        );
+      })}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/pages/timeline/TimelinePostListView.tsx b/FrontEnd/src/pages/timeline/TimelinePostListView.tsx
deleted file mode 100644
index f878b004..00000000
--- a/FrontEnd/src/pages/timeline/TimelinePostListView.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Fragment } from "react";
-import * as React from "react";
-
-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 {
-  posts: HttpTimelinePostInfo[];
-  onReload: () => void;
-}
-
-const TimelinePostListView: React.FC<TimelinePostListViewProps> = (props) => {
-  const { 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 (
-    <>
-      {groupedPosts.map((group) => {
-        return (
-          <Fragment key={group.date.toDateString()}>
-            <TimelineDateLabel date={group.date} />
-            {group.posts.map((post) => {
-              return (
-                <TimelinePostView
-                  key={post.id}
-                  post={post}
-                  onChanged={onReload}
-                  onDeleted={onReload}
-                />
-              );
-            })}
-          </Fragment>
-        );
-      })}
-    </>
-  );
-};
-
-export default TimelinePostListView;
diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.css b/FrontEnd/src/pages/timeline/TimelinePostView.css
index 2cd8cd6b..229b4a7a 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostView.css
+++ b/FrontEnd/src/pages/timeline/TimelinePostView.css
@@ -1,13 +1,3 @@
-.timeline-post {
-  position: relative;
-  padding: 0.5em;
-}
-
-.timeline-post-card {
-  position: relative;
-  padding: 0.5em 0.5em 0.5em 4em;
-}
-
 .timeline-post-header {
   display: flex;
   align-items: center;
@@ -19,10 +9,17 @@
   height: 2em;
 }
 
-.timeline-post-delete-button {
+.timeline-post-edit-button {
+  float: right;
+}
+
+.timeline-post-options-mask {
   position: absolute;
-  right: 0;
-  bottom: 0;
+  inset: 0;
+  background-color: hsla(0, 0%, 100%, 0.9);
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
 }
 
 .timeline-post-content {
diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx
index bdd2e3ef..2648fa21 100644
--- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx
+++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx
@@ -1,5 +1,4 @@
 import { useState } from "react";
-import classNames from "classnames";
 
 import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";
 
@@ -9,13 +8,14 @@ import { useClickOutside } from "@/utilities/hooks";
 
 import UserAvatar from "@/views/common/user/UserAvatar";
 import { useDialog } from "@/views/common/dialog";
-import Card from "@/views/common/Card";
 import FlatButton from "@/views/common/button/FlatButton";
 import ConfirmDialog from "@/views/common/dialog/ConfirmDialog";
-import TimelineLine from "./TimelineLine";
 import TimelinePostContentView from "./TimelinePostContentView";
 import IconButton from "@/views/common/button/IconButton";
 
+import TimelinePostContainer from "./TimelinePostContainer";
+import TimelinePostCard from "./TimelinePostCard";
+
 import "./TimelinePostView.css";
 
 export interface TimelinePostViewProps {
@@ -26,7 +26,7 @@ export interface TimelinePostViewProps {
 }
 
 export function TimelinePostView(props: TimelinePostViewProps) {
-  const { post, className, onDeleted } = props;
+  const { post, onDeleted } = props;
 
   const [operationMaskVisible, setOperationMaskVisible] =
     useState<boolean>(false);
@@ -43,12 +43,8 @@ export function TimelinePostView(props: TimelinePostViewProps) {
   useClickOutside(maskElement, () => setOperationMaskVisible(false));
 
   return (
-    <div
-      id={`timeline-post-${post.id}`}
-      className={classNames("timeline-post cru-primary", className)}
-    >
-      <TimelineLine center="node" />
-      <Card className="timeline-post-card">
+    <TimelinePostContainer>
+      <TimelinePostCard className="cru-primary">
         {post.editable && (
           <IconButton
             icon="chevron-down"
@@ -98,7 +94,7 @@ export function TimelinePostView(props: TimelinePostViewProps) {
             />
           </div>
         ) : null}
-      </Card>
+      </TimelinePostCard>
       <ConfirmDialog
         title="timeline.post.deleteDialog.title"
         body="timeline.post.deleteDialog.prompt"
@@ -114,7 +110,7 @@ export function TimelinePostView(props: TimelinePostViewProps) {
         }}
         {...dialogPropsMap.delete}
       />
-    </div>
+    </TimelinePostContainer>
   );
 }
 
-- 
cgit v1.2.3

