From 538d6830a0022b49b99695095d85e567b0c86e71 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 30 Jul 2023 23:47:53 +0800 Subject: ... --- FrontEnd/src/pages/timeline/TimelinePostView.tsx | 149 +++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 FrontEnd/src/pages/timeline/TimelinePostView.tsx (limited to 'FrontEnd/src/pages/timeline/TimelinePostView.tsx') diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx new file mode 100644 index 00000000..f7aec169 --- /dev/null +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -0,0 +1,149 @@ +import * as React from "react"; +import classnames from "classnames"; + +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; + +import { pushAlert } from "@/services/alert"; + +import { useClickOutside } from "@/utilities/hooks"; + +import UserAvatar from "@/views/common/user/UserAvatar"; +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"; + +export interface TimelinePostViewProps { + post: HttpTimelinePostInfo; + className?: string; + style?: React.CSSProperties; + cardStyle?: React.CSSProperties; + onChanged: (post: HttpTimelinePostInfo) => void; + onDeleted: () => void; +} + +const TimelinePostView: React.FC = (props) => { + const { post, className, style, cardStyle, onChanged, onDeleted } = props; + + const [operationMaskVisible, setOperationMaskVisible] = + React.useState(false); + const [dialog, setDialog] = React.useState< + "delete" | "changeproperty" | null + >(null); + + const [maskElement, setMaskElement] = React.useState( + null, + ); + + useClickOutside(maskElement, () => setOperationMaskVisible(false)); + + const cardRef = React.useRef(null); + React.useEffect(() => { + const cardIntersectionObserver = new IntersectionObserver(([e]) => { + if (e.intersectionRatio > 0) { + if (cardRef.current != null) { + cardRef.current.style.animationName = "timeline-post-enter"; + } + } + }); + if (cardRef.current) { + cardIntersectionObserver.observe(cardRef.current); + } + + return () => { + cardIntersectionObserver.disconnect(); + }; + }, []); + + return ( +
+ + + {post.editable ? ( + { + setOperationMaskVisible(true); + e.stopPropagation(); + }} + /> + ) : null} +
+ + + + {post.author.nickname} + + {new Date(post.time).toLocaleTimeString()} + + + +
+
+ +
+ {operationMaskVisible ? ( +
{ + setOperationMaskVisible(false); + }} + > + { + setDialog("changeproperty"); + e.stopPropagation(); + }} + /> + { + setDialog("delete"); + e.stopPropagation(); + }} + /> +
+ ) : null} +
+ { + setDialog(null); + setOperationMaskVisible(false); + }} + onConfirm={() => { + void getHttpTimelineClient() + .deletePost(post.timelineOwnerV2, post.timelineNameV2, post.id) + .then(onDeleted, () => { + pushAlert({ + type: "danger", + message: "timeline.deletePostFailed", + }); + }); + }} + /> +
+ ); +}; + +export default TimelinePostView; -- cgit v1.2.3 From 710ff9d3d2e55113798c39b0595f8f71b12091ef Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 31 Jul 2023 21:16:27 +0800 Subject: ... --- FrontEnd/src/pages/timeline/TimelinePostView.css | 30 +++++++ FrontEnd/src/pages/timeline/TimelinePostView.tsx | 102 ++++++++--------------- FrontEnd/src/views/common/dialog/index.ts | 14 +++- 3 files changed, 78 insertions(+), 68 deletions(-) create mode 100644 FrontEnd/src/pages/timeline/TimelinePostView.css (limited to 'FrontEnd/src/pages/timeline/TimelinePostView.tsx') diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.css b/FrontEnd/src/pages/timeline/TimelinePostView.css new file mode 100644 index 00000000..2cd8cd6b --- /dev/null +++ b/FrontEnd/src/pages/timeline/TimelinePostView.css @@ -0,0 +1,30 @@ +.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; +} + +.timeline-post-author-avatar { + border-radius: 50%; + width: 2em; + height: 2em; +} + +.timeline-post-delete-button { + position: absolute; + right: 0; + bottom: 0; +} + +.timeline-post-content { + white-space: pre-line; +} \ No newline at end of file diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx index f7aec169..bdd2e3ef 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -1,5 +1,5 @@ -import * as React from "react"; -import classnames from "classnames"; +import { useState } from "react"; +import classNames from "classnames"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; @@ -8,6 +8,7 @@ import { pushAlert } from "@/services/alert"; 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"; @@ -15,92 +16,68 @@ import TimelineLine from "./TimelineLine"; import TimelinePostContentView from "./TimelinePostContentView"; import IconButton from "@/views/common/button/IconButton"; +import "./TimelinePostView.css"; + export interface TimelinePostViewProps { post: HttpTimelinePostInfo; className?: string; - style?: React.CSSProperties; - cardStyle?: React.CSSProperties; onChanged: (post: HttpTimelinePostInfo) => void; onDeleted: () => void; } -const TimelinePostView: React.FC = (props) => { - const { post, className, style, cardStyle, onChanged, onDeleted } = props; +export function TimelinePostView(props: TimelinePostViewProps) { + const { post, className, onDeleted } = props; const [operationMaskVisible, setOperationMaskVisible] = - React.useState(false); - const [dialog, setDialog] = React.useState< - "delete" | "changeproperty" | null - >(null); + useState(false); - const [maskElement, setMaskElement] = React.useState( - null, - ); + const { switchDialog, dialogPropsMap } = useDialog(["delete"], { + onClose: { + delete: () => { + setOperationMaskVisible(false); + }, + }, + }); + const [maskElement, setMaskElement] = useState(null); useClickOutside(maskElement, () => setOperationMaskVisible(false)); - const cardRef = React.useRef(null); - React.useEffect(() => { - const cardIntersectionObserver = new IntersectionObserver(([e]) => { - if (e.intersectionRatio > 0) { - if (cardRef.current != null) { - cardRef.current.style.animationName = "timeline-post-enter"; - } - } - }); - if (cardRef.current) { - cardIntersectionObserver.observe(cardRef.current); - } - - return () => { - cardIntersectionObserver.disconnect(); - }; - }, []); - return (
- - {post.editable ? ( + + {post.editable && ( { setOperationMaskVisible(true); e.stopPropagation(); }} /> - ) : null} -
- - - - {post.author.nickname} - - {new Date(post.time).toLocaleTimeString()} - - - + )} +
+ + + {post.author.nickname} + + + {new Date(post.time).toLocaleTimeString()} +
-
+
{operationMaskVisible ? (
{ setOperationMaskVisible(false); }} @@ -108,7 +85,6 @@ const TimelinePostView: React.FC = (props) => { { - setDialog("changeproperty"); e.stopPropagation(); }} /> @@ -116,7 +92,7 @@ const TimelinePostView: React.FC = (props) => { text="delete" color="danger" onClick={(e) => { - setDialog("delete"); + switchDialog("delete"); e.stopPropagation(); }} /> @@ -126,11 +102,6 @@ const TimelinePostView: React.FC = (props) => { { - setDialog(null); - setOperationMaskVisible(false); - }} onConfirm={() => { void getHttpTimelineClient() .deletePost(post.timelineOwnerV2, post.timelineNameV2, post.id) @@ -141,9 +112,10 @@ const TimelinePostView: React.FC = (props) => { }); }); }} + {...dialogPropsMap.delete} />
); -}; +} export default TimelinePostView; diff --git a/FrontEnd/src/views/common/dialog/index.ts b/FrontEnd/src/views/common/dialog/index.ts index e37b9ed2..59f15791 100644 --- a/FrontEnd/src/views/common/dialog/index.ts +++ b/FrontEnd/src/views/common/dialog/index.ts @@ -18,14 +18,19 @@ type DialogPropsMap = DialogMap< export function useDialog( dialogs: D[], - initDialog?: D | null, + options?: { + initDialog?: D | null; + onClose?: { + [K in D]?: () => void; + }; + }, ): { dialog: D | null; switchDialog: (newDialog: D | null) => void; dialogPropsMap: DialogPropsMap; createDialogSwitch: (newDialog: D | null) => () => void; } { - const [dialog, setDialog] = useState(initDialog ?? null); + const [dialog, setDialog] = useState(options?.initDialog ?? null); const [dialogKeys, setDialogKeys] = useState>( () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap, @@ -47,7 +52,10 @@ export function useDialog( { key: `${d}-${dialogKeys[d]}`, open: dialog === d, - onClose: () => switchDialog(null), + onClose: () => { + switchDialog(null); + options?.onClose?.[d]?.(); + }, }, ]), ) as DialogPropsMap, -- cgit v1.2.3 From e6ccc0174a86a0ade240e6551228598cd81f984b Mon Sep 17 00:00:00 2001 From: crupest 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/TimelinePostView.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 = (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(null); - const [posts, setPosts] = React.useState(null); - const [signalrState, setSignalrState] = React.useState( + const [timeline, setTimeline] = useState(null); + const [posts, setPosts] = useState(null); + const [signalrState, setSignalrState] = useState( 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 = (props) => { ); }, [timelineOwner, timelineName, timelineReloadKey]); - React.useEffect(() => { + useEffect(() => { getHttpTimelineClient() .listPost(timelineOwner, timelineName, 1) .then( @@ -110,7 +103,7 @@ const Timeline: React.FC = (props) => { ); }, [timelineOwner, timelineName, postsReloadKey]); - React.useEffect(() => { + useEffect(() => { const timelinePostUpdate$ = getTimelinePostUpdate$( timelineOwner, timelineName, @@ -154,33 +147,16 @@ const Timeline: React.FC = (props) => { }, currentPage < totalPage); if (error === "offline") { - return ( -
- Offline. -
- ); + return
Offline.
; } else if (error === "notfound") { - return ( -
- Not exist. -
- ); + return
Not exist.
; } else if (error === "forbid") { - return ( -
- Forbid. -
- ); + return
Forbid.
; } else if (error === "error") { - return ( -
- Error. -
- ); + return
Error.
; } return ( <> - {timeline == null && posts == null && } {timeline && ( = (props) => { /> )} {posts && ( -
- - {timeline?.postable ? ( +
+ {timeline?.postable && ( - ) : user == null ? ( - - ) : null} - + )} +
)} ); -}; +} 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 ( = ({ date }) => { +export default function TimelineDateLabel({ date }: { date: Date }) { return ( -
- -
+ +
{date.toLocaleDateString()}
-
+ ); -}; - -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 { - height?: number | string; - className?: string; - style?: React.CSSProperties; -} - -const TimelineEmptyItem: React.FC = (props) => { - const { height, style, className, center, ...lineProps } = props; - - return ( -
- -
- ); -}; - -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 = ({ - startSegmentLength, - center, - current, - className, - style, -}) => { - return ( -
-
- {center !== "none" ? ( -
-
- {center === "loading" ? ( - - - - ) : null} -
- ) : null} - {center !== "loading" ?
: null} - {current &&
} -
- ); -}; - -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 ( - - ); -}; - -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 ( + + {children} + + ); +} 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 ( +
+ {children} +
+ ); +} 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 = (props) => { - const { timeline, style, className, onPosted } = props; + const { timeline, className, onPosted } = props; const { t } = useTranslation(); @@ -195,7 +195,7 @@ const TimelinePostEdit: React.FC = (props) => { }; return ( - + {showMarkdown ? ( = ({ - className, - style, - children, -}) => { - return ( -
- - {children} -
- ); -}; - -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 ( - -
- }} - /> -
-
- ); -} 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 ( +
+
+ {groupedPosts.map((group) => { + return ( + + + {group.posts.map((post) => { + return ( + + ); + })} + + ); + })} +
+ ); +} 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 = (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 ( - - - {group.posts.map((post) => { - return ( - - ); - })} - - ); - })} - - ); -}; - -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(false); @@ -43,12 +43,8 @@ export function TimelinePostView(props: TimelinePostViewProps) { useClickOutside(maskElement, () => setOperationMaskVisible(false)); return ( -
- - + + {post.editable && (
) : null} - + -
+ ); } -- cgit v1.2.3 From 4133d7122a54faf85458151d36c5fc040db7baef Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 1 Aug 2023 00:40:18 +0800 Subject: ... --- FrontEnd/src/pages/setting/index.tsx | 2 +- FrontEnd/src/pages/timeline/TimelinePostCard.tsx | 4 +-- .../src/pages/timeline/TimelinePostContainer.tsx | 4 +-- FrontEnd/src/pages/timeline/TimelinePostEdit.tsx | 39 ++++++++++------------ FrontEnd/src/pages/timeline/TimelinePostView.tsx | 6 ++-- 5 files changed, 25 insertions(+), 30 deletions(-) (limited to 'FrontEnd/src/pages/timeline/TimelinePostView.tsx') diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 12a7670e..8e8ae488 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -247,7 +247,7 @@ export default function SettingPage() { ]); return ( - + {user ? ( diff --git a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx index 83479349..f3743915 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostCard.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostCard.tsx @@ -5,7 +5,7 @@ import Card from "@/views/common/Card"; import "./TimelinePostCard.css"; -export interface TimelinePostEditCardProps { +interface TimelinePostCardProps { className?: string; children?: ReactNode; } @@ -13,7 +13,7 @@ export interface TimelinePostEditCardProps { export default function TimelinePostCard({ className, children, -}: TimelinePostEditCardProps) { +}: TimelinePostCardProps) { return ( {children} diff --git a/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx b/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx index 4697268b..9dc211b2 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostContainer.tsx @@ -3,7 +3,7 @@ import classNames from "classnames"; import "./TimelinePostContainer.css"; -export interface TimelinePostEditCardProps { +interface TimelinePostContainerProps { className?: string; children?: ReactNode; } @@ -11,7 +11,7 @@ export interface TimelinePostEditCardProps { export default function TimelinePostContainer({ className, children, -}: TimelinePostEditCardProps) { +}: TimelinePostContainerProps) { return (
{children} diff --git a/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx b/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx index b0cc763a..cd5a2d8d 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import { useState, useEffect, ChangeEventHandler } from "react"; import { useTranslation } from "react-i18next"; import { UiLogicError } from "@/common"; @@ -28,11 +28,10 @@ interface TimelinePostEditTextProps { disabled: boolean; onChange: (text: string) => void; className?: string; - style?: React.CSSProperties; } -const TimelinePostEditText: React.FC = (props) => { - const { text, disabled, onChange, className, style } = props; +function TimelinePostEditText(props: TimelinePostEditTextProps) { + const { text, disabled, onChange, className } = props; return (