From 47587812b809fee2a95c76266d9d0e42fc4ac1ca Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 14:14:28 +0800 Subject: ... --- .../src/views/timeline-common/CollapseButton.tsx | 23 ++ .../timeline-common/ConnectionStatusBadge.tsx | 39 +++ .../src/views/timeline-common/MarkdownPostEdit.tsx | 205 +++++++++++++++ .../timeline-common/PostPropertyChangeDialog.tsx | 36 +++ FrontEnd/src/views/timeline-common/Timeline.tsx | 143 ++++++++++ .../views/timeline-common/TimelineDateLabel.tsx | 19 ++ .../src/views/timeline-common/TimelineLine.tsx | 51 ++++ .../src/views/timeline-common/TimelineLoading.tsx | 18 ++ .../src/views/timeline-common/TimelineMember.tsx | 195 ++++++++++++++ .../timeline-common/TimelinePageCardTemplate.tsx | 158 +++++++++++ .../views/timeline-common/TimelinePageTemplate.tsx | 190 ++++++++++++++ .../timeline-common/TimelinePagedPostListView.tsx | 43 +++ .../timeline-common/TimelinePostContentView.tsx | 197 ++++++++++++++ .../TimelinePostDeleteConfirmDialog.tsx | 37 +++ .../src/views/timeline-common/TimelinePostEdit.tsx | 291 +++++++++++++++++++++ .../views/timeline-common/TimelinePostListView.tsx | 79 ++++++ .../src/views/timeline-common/TimelinePostView.tsx | 151 +++++++++++ .../TimelinePropertyChangeDialog.tsx | 87 ++++++ FrontEnd/src/views/timeline-common/TimelineTop.tsx | 27 ++ .../src/views/timeline-common/timeline-common.sass | 259 ++++++++++++++++++ 20 files changed, 2248 insertions(+) create mode 100644 FrontEnd/src/views/timeline-common/CollapseButton.tsx create mode 100644 FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx create mode 100644 FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx create mode 100644 FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx create mode 100644 FrontEnd/src/views/timeline-common/Timeline.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelineDateLabel.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelineLine.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelineLoading.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelineMember.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePostListView.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePostView.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx create mode 100644 FrontEnd/src/views/timeline-common/TimelineTop.tsx create mode 100644 FrontEnd/src/views/timeline-common/timeline-common.sass (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/views/timeline-common/CollapseButton.tsx b/FrontEnd/src/views/timeline-common/CollapseButton.tsx new file mode 100644 index 00000000..12a3b710 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/CollapseButton.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import classnames from "classnames"; + +const CollapseButton: React.FC<{ + collapse: boolean; + onClick: () => void; + className?: string; + style?: React.CSSProperties; +}> = ({ collapse, onClick, className, style }) => { + return ( + + ); +}; + +export default CollapseButton; diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx new file mode 100644 index 00000000..df43d8d2 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import classnames from "classnames"; +import { HubConnectionState } from "@microsoft/signalr"; +import { useTranslation } from "react-i18next"; + +export interface ConnectionStatusBadgeProps { + status: HubConnectionState; + className?: string; + style?: React.CSSProperties; +} + +const classNameMap: Record = { + Connected: "success", + Connecting: "warning", + Disconnected: "danger", + Disconnecting: "warning", + Reconnecting: "warning", +}; + +const ConnectionStatusBadge: React.FC = (props) => { + const { status, className, style } = props; + + const { t } = useTranslation(); + + return ( +
+ {t(`connectionState.${status}`)} +
+ ); +}; + +export default ConnectionStatusBadge; diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx new file mode 100644 index 00000000..1514d28f --- /dev/null +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -0,0 +1,205 @@ +import React from "react"; +import classnames from "classnames"; +import { Form, Spinner } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; +import { Prompt } from "react-router"; + +import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; + +import FlatButton from "../common/FlatButton"; +import TabPages from "../common/TabPages"; +import TimelinePostBuilder from "@/services/TimelinePostBuilder"; +import ConfirmDialog from "../common/ConfirmDialog"; + +export interface MarkdownPostEditProps { + timeline: string; + onPosted: (post: HttpTimelinePostInfo) => void; + onPostError: () => void; + onClose: () => void; + className?: string; + style?: React.CSSProperties; +} + +const MarkdownPostEdit: React.FC = ({ + timeline: timelineName, + onPosted, + onClose, + onPostError, + className, + style, +}) => { + const { t } = useTranslation(); + + const [canLeave, setCanLeave] = React.useState(true); + + const [process, setProcess] = React.useState(false); + + const [showLeaveConfirmDialog, setShowLeaveConfirmDialog] = + React.useState(false); + + const [text, _setText] = React.useState(""); + const [images, _setImages] = React.useState<{ file: File; url: string }[]>( + [] + ); + const [previewHtml, _setPreviewHtml] = React.useState(""); + + const _builder = React.useRef(null); + + const getBuilder = (): TimelinePostBuilder => { + if (_builder.current == null) { + const builder = new TimelinePostBuilder(() => { + setCanLeave(builder.isEmpty); + _setText(builder.text); + _setImages(builder.images); + _setPreviewHtml(builder.renderHtml()); + }); + _builder.current = builder; + } + return _builder.current; + }; + + const canSend = text.length > 0; + + React.useEffect(() => { + return () => { + getBuilder().dispose(); + }; + }, []); + + React.useEffect(() => { + window.onbeforeunload = (): unknown => { + if (!canLeave) { + return t("timeline.confirmLeave"); + } + }; + + return () => { + window.onbeforeunload = null; + }; + }, [canLeave, t]); + + const send = async (): Promise => { + setProcess(true); + try { + const dataList = await getBuilder().build(); + const post = await getHttpTimelineClient().postPost(timelineName, { + dataList, + }); + onPosted(post); + onClose(); + } catch (e) { + setProcess(false); + onPostError(); + } + }; + + return ( + <> + + + ) : ( + <> + { + if (canLeave) { + onClose(); + } else { + setShowLeaveConfirmDialog(true); + } + }} + > + {t("operationDialog.cancel")} + + + {t("timeline.send")} + + + ) + } + pages={[ + { + id: "text", + tabText: "edit", + page: ( + { + getBuilder().setMarkdownText(event.currentTarget.value); + }} + /> + ), + }, + { + id: "images", + tabText: "image", + page: ( +
+ {images.map((image, index) => ( +
+ + { + getBuilder().deleteImage(index); + }} + /> +
+ ))} + ) => { + const { files } = event.currentTarget; + if (files != null && files.length !== 0) { + getBuilder().appendImage(files[0]); + } + }} + disabled={process} + /> +
+ ), + }, + { + id: "preview", + tabText: "preview", + page: ( +
+ ), + }, + ]} + /> + {showLeaveConfirmDialog && ( + setShowLeaveConfirmDialog(false)} + onConfirm={onClose} + title="timeline.dropDraft" + body="timeline.confirmLeave" + /> + )} + + ); +}; + +export default MarkdownPostEdit; diff --git a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx new file mode 100644 index 00000000..21c5272e --- /dev/null +++ b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; + +import OperationDialog from "../common/OperationDialog"; + +function PostPropertyChangeDialog(props: { + onClose: () => void; + post: HttpTimelinePostInfo; + onSuccess: (post: HttpTimelinePostInfo) => void; +}): React.ReactElement | null { + const { onClose, post, onSuccess } = props; + + return ( + { + return getHttpTimelineClient().patchPost(post.timelineName, post.id, { + time: time === "" ? undefined : new Date(time).toISOString(), + }); + }} + onSuccessAndClose={onSuccess} + /> + ); +} + +export default PostPropertyChangeDialog; diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx new file mode 100644 index 00000000..40619e64 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/Timeline.tsx @@ -0,0 +1,143 @@ +import React from "react"; +import { HubConnectionState } from "@microsoft/signalr"; + +import { + HttpForbiddenError, + HttpNetworkError, + HttpNotFoundError, +} from "http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; + +import { getTimelinePostUpdate$ } from "@/services/timeline"; + +import TimelinePagedPostListView from "./TimelinePagedPostListView"; +import TimelineTop from "./TimelineTop"; +import TimelineLoading from "./TimelineLoading"; + +export interface TimelineProps { + className?: string; + style?: React.CSSProperties; + timelineName?: string; + reloadKey: number; + onReload: () => void; + onConnectionStateChanged?: (state: HubConnectionState) => void; +} + +const Timeline: React.FC = (props) => { + const { timelineName, className, style, reloadKey } = props; + + const [state, setState] = React.useState< + "loading" | "loaded" | "offline" | "notexist" | "forbid" | "error" + >("loading"); + const [posts, setPosts] = React.useState([]); + + React.useEffect(() => { + setState("loading"); + setPosts([]); + }, [timelineName]); + + const onReload = React.useRef<() => void>(props.onReload); + + React.useEffect(() => { + onReload.current = props.onReload; + }, [props.onReload]); + + const onConnectionStateChanged = React.useRef< + ((state: HubConnectionState) => void) | null + >(null); + + React.useEffect(() => { + onConnectionStateChanged.current = props.onConnectionStateChanged ?? null; + }, [props.onConnectionStateChanged]); + + React.useEffect(() => { + if (timelineName != null && state === "loaded") { + const timelinePostUpdate$ = getTimelinePostUpdate$(timelineName); + const subscription = timelinePostUpdate$.subscribe( + ({ update, state }) => { + if (update) { + onReload.current(); + } + onConnectionStateChanged.current?.(state); + } + ); + return () => { + subscription.unsubscribe(); + }; + } + }, [timelineName, state]); + + React.useEffect(() => { + if (timelineName != null) { + let subscribe = true; + + void getHttpTimelineClient() + .listPost(timelineName) + .then( + (data) => { + if (subscribe) { + setState("loaded"); + setPosts(data); + } + }, + (error) => { + if (error instanceof HttpNetworkError) { + setState("offline"); + } else if (error instanceof HttpForbiddenError) { + setState("forbid"); + } else if (error instanceof HttpNotFoundError) { + setState("notexist"); + } else { + console.error(error); + setState("error"); + } + } + ); + + return () => { + subscribe = false; + }; + } + }, [timelineName, reloadKey]); + + switch (state) { + case "loading": + return ; + case "offline": + return ( +
+ Offline. +
+ ); + case "notexist": + return ( +
+ Not exist. +
+ ); + case "forbid": + return ( +
+ Forbid. +
+ ); + case "error": + return ( +
+ Error. +
+ ); + default: + return ( + <> + + + + ); + } +}; + +export default Timeline; diff --git a/FrontEnd/src/views/timeline-common/TimelineDateLabel.tsx b/FrontEnd/src/views/timeline-common/TimelineDateLabel.tsx new file mode 100644 index 00000000..80968ee2 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelineDateLabel.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import TimelineLine from "./TimelineLine"; + +export interface TimelineDateItemProps { + date: Date; +} + +const TimelineDateLabel: React.FC = ({ date }) => { + return ( +
+ +
+ {date.toLocaleDateString()} +
+
+ ); +}; + +export default TimelineDateLabel; diff --git a/FrontEnd/src/views/timeline-common/TimelineLine.tsx b/FrontEnd/src/views/timeline-common/TimelineLine.tsx new file mode 100644 index 00000000..0a828b32 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelineLine.tsx @@ -0,0 +1,51 @@ +import 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/views/timeline-common/TimelineLoading.tsx b/FrontEnd/src/views/timeline-common/TimelineLoading.tsx new file mode 100644 index 00000000..fc42f4b4 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelineLoading.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import TimelineTop from "./TimelineTop"; + +const TimelineLoading: React.FC = () => { + return ( + + ); +}; + +export default TimelineLoading; diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx new file mode 100644 index 00000000..3d4de8b8 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx @@ -0,0 +1,195 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; + +import { convertI18nText, I18nText } from "@/common"; + +import { HttpUser } from "http/user"; +import { getHttpSearchClient } from "http/search"; + +import SearchInput from "../common/SearchInput"; +import UserAvatar from "../common/user/UserAvatar"; +import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; + +const TimelineMemberItem: React.FC<{ + user: HttpUser; + add?: boolean; + onAction?: (username: string) => void; +}> = ({ user, add, onAction }) => { + const { t } = useTranslation(); + + return ( + + + + + + + {user.nickname} + + {"@" + user.username} + + + {onAction ? ( + + + + ) : null} + + + ); +}; + +const TimelineMemberUserSearch: React.FC<{ + timeline: HttpTimelineInfo; + onChange: () => void; +}> = ({ timeline, onChange }) => { + const { t } = useTranslation(); + + const [userSearchText, setUserSearchText] = useState(""); + const [userSearchState, setUserSearchState] = useState< + | { + type: "users"; + data: HttpUser[]; + } + | { type: "error"; data: I18nText } + | { type: "loading" } + | { type: "init" } + >({ type: "init" }); + + return ( + <> + { + setUserSearchText(v); + }} + loading={userSearchState.type === "loading"} + onButtonClick={() => { + if (userSearchText === "") { + setUserSearchState({ + type: "error", + data: "login.emptyUsername", + }); + return; + } + setUserSearchState({ type: "loading" }); + getHttpSearchClient() + .searchUsers(userSearchText) + .then( + (users) => { + users = users.filter( + (user) => + timeline.members.findIndex( + (m) => m.username === user.username + ) === -1 && timeline.owner.username !== user.username + ); + setUserSearchState({ type: "users", data: users }); + }, + (e) => { + setUserSearchState({ + type: "error", + data: { type: "custom", value: String(e) }, + }); + } + ); + }} + /> + {(() => { + if (userSearchState.type === "users") { + const users = userSearchState.data; + if (users.length === 0) { + return
{t("timeline.member.noUserAvailableToAdd")}
; + } else { + return ( + + {users.map((user) => ( + { + void getHttpTimelineClient() + .memberPut(timeline.name, user.username) + .then(() => { + setUserSearchText(""); + setUserSearchState({ type: "init" }); + onChange(); + }); + }} + /> + ))} + + ); + } + } else if (userSearchState.type === "error") { + return ( +
+ {convertI18nText(userSearchState.data, t)} +
+ ); + } + })()} + + ); +}; + +export interface TimelineMemberProps { + timeline: HttpTimelineInfo; + onChange: () => void; +} + +const TimelineMember: React.FC = (props) => { + const { timeline, onChange } = props; + const members = [timeline.owner, ...timeline.members]; + + return ( + + + {members.map((member, index) => ( + { + void getHttpTimelineClient() + .memberDelete(timeline.name, member.username) + .then(onChange); + } + : undefined + } + /> + ))} + + {timeline.manageable ? ( + + ) : null} + + ); +}; + +export default TimelineMember; + +export interface TimelineMemberDialogProps extends TimelineMemberProps { + open: boolean; + onClose: () => void; +} + +export const TimelineMemberDialog: React.FC = ( + props +) => { + return ( + + + + ); +}; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx new file mode 100644 index 00000000..038ea3ab --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -0,0 +1,158 @@ +import React from "react"; +import classnames from "classnames"; +import { useTranslation } from "react-i18next"; + +import { getHttpHighlightClient } from "http/highlight"; +import { getHttpBookmarkClient } from "http/bookmark"; + +import { useUser } from "@/services/user"; +import { pushAlert } from "@/services/alert"; +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; + +import { useIsSmallScreen } from "@/utilities/mediaQuery"; + +import { TimelinePageCardProps } from "./TimelinePageTemplate"; + +import CollapseButton from "./CollapseButton"; +import { TimelineMemberDialog } from "./TimelineMember"; +import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; +import ConnectionStatusBadge from "./ConnectionStatusBadge"; +import { MenuItems, PopupMenu } from "../common/Menu"; +import FullPage from "../common/FullPage"; + +export interface TimelineCardTemplateProps extends TimelinePageCardProps { + infoArea: React.ReactElement; + manageItems?: MenuItems; + dialog: string | "property" | "member" | null; + setDialog: (dialog: "property" | "member" | null) => void; +} + +const TimelinePageCardTemplate: React.FC = ({ + timeline, + collapse, + toggleCollapse, + infoArea, + manageItems, + connectionStatus, + onReload, + className, + dialog, + setDialog, +}) => { + const { t } = useTranslation(); + + const isSmallScreen = useIsSmallScreen(); + + const user = useUser(); + + const content = ( + <> + {infoArea} +

{timeline.description}

+ + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + +
+ { + getHttpHighlightClient() + [timeline.isHighlight ? "delete" : "put"](timeline.name) + .then(onReload, () => { + pushAlert({ + message: timeline.isHighlight + ? "timeline.removeHighlightFail" + : "timeline.addHighlightFail", + type: "danger", + }); + }); + } + : undefined + } + /> + {user != null ? ( + { + getHttpBookmarkClient() + [timeline.isBookmark ? "delete" : "put"](timeline.name) + .then(onReload, () => { + pushAlert({ + message: timeline.isBookmark + ? "timeline.removeBookmarkFail" + : "timeline.addBookmarkFail", + type: "danger", + }); + }); + }} + /> + ) : null} + setDialog("member")} + /> + {manageItems != null ? ( + + + + ) : null} +
+ + ); + + return ( + <> +
+
+ + +
+ {isSmallScreen ? ( + + {content} + + ) : ( +
{content}
+ )} +
+ {(() => { + if (dialog === "member") { + return ( + setDialog(null)} + open + onChange={onReload} + /> + ); + } else if (dialog === "property") { + return ( + setDialog(null)} + open + onChange={onReload} + /> + ); + } + })()} + + ); +}; + +export default TimelinePageCardTemplate; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx new file mode 100644 index 00000000..44926cc6 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx @@ -0,0 +1,190 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Container } from "react-bootstrap"; +import { HubConnectionState } from "@microsoft/signalr"; + +import { HttpNetworkError, HttpNotFoundError } from "http/common"; +import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; + +import { getAlertHost } from "@/services/alert"; + +import Timeline from "./Timeline"; +import TimelinePostEdit from "./TimelinePostEdit"; + +import useReverseScrollPositionRemember from "@/utilities/useReverseScrollPositionRemember"; +import { generatePalette, setPalette } from "@/palette"; + +export interface TimelinePageCardProps { + timeline: HttpTimelineInfo; + collapse: boolean; + toggleCollapse: () => void; + connectionStatus: HubConnectionState; + className?: string; + onReload: () => void; +} + +export interface TimelinePageTemplateProps { + timelineName: string; + notFoundI18nKey: string; + reloadKey: number; + onReload: () => void; + CardComponent: React.ComponentType; +} + +const TimelinePageTemplate: React.FC = (props) => { + const { timelineName, reloadKey, onReload, CardComponent } = props; + + const { t } = useTranslation(); + + const [state, setState] = React.useState< + "loading" | "done" | "offline" | "notexist" | "error" + >("loading"); + const [timeline, setTimeline] = React.useState(null); + + const [connectionStatus, setConnectionStatus] = + React.useState(HubConnectionState.Connecting); + + useReverseScrollPositionRemember(); + + React.useEffect(() => { + setState("loading"); + setTimeline(null); + }, [timelineName]); + + React.useEffect(() => { + let subscribe = true; + void getHttpTimelineClient() + .getTimeline(timelineName) + .then( + (data) => { + if (subscribe) { + setState("done"); + setTimeline(data); + } + }, + (error) => { + if (subscribe) { + if (error instanceof HttpNetworkError) { + setState("offline"); + } else if (error instanceof HttpNotFoundError) { + setState("notexist"); + } else { + console.error(error); + setState("error"); + } + setTimeline(null); + } + } + ); + return () => { + subscribe = false; + }; + }, [timelineName, reloadKey]); + + React.useEffect(() => { + if (timeline != null && timeline.color != null) { + return setPalette(generatePalette({ primary: timeline.color })); + } + }, [timeline]); + + const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState(0); + + const [timelineReloadKey, setTimelineReloadKey] = React.useState(0); + + const reloadTimeline = (): void => { + setTimelineReloadKey((old) => old + 1); + }; + + const onPostEditHeightChange = React.useCallback((height: number): void => { + setBottomSpaceHeight(height); + if (height === 0) { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.removeProperty("margin-bottom"); + } + } else { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.marginBottom = `${height}px`; + } + } + }, []); + + const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`; + + const [cardCollapse, setCardCollapse] = React.useState(true); + + React.useEffect(() => { + const savedCollapse = window.localStorage.getItem( + cardCollapseLocalStorageKey + ); + setCardCollapse(savedCollapse == null ? true : savedCollapse === "true"); + }, [cardCollapseLocalStorageKey]); + + const toggleCardCollapse = (): void => { + const newState = !cardCollapse; + setCardCollapse(newState); + window.localStorage.setItem( + cardCollapseLocalStorageKey, + newState.toString() + ); + }; + + return ( + <> + {timeline != null ? ( + + ) : null} + + {(() => { + if (state === "offline") { + // TODO: i18n + return

Offline!

; + } else if (state === "notexist") { + return

{t(props.notFoundI18nKey)}

; + } else if (state === "error") { + // TODO: i18n + return

Error!

; + } else { + return ( + + ); + } + })()} +
+ {timeline != null && timeline.postable ? ( + <> +
+ + + ) : null} + + ); +}; + +export default TimelinePageTemplate; diff --git a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx new file mode 100644 index 00000000..d569a2d7 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +import { HttpTimelinePostInfo } from "http/timeline"; + +import useScrollToTop from "@/utilities/useScrollToTop"; + +import TimelinePostListView from "./TimelinePostListView"; + +export interface TimelinePagedPostListViewProps { + className?: string; + style?: React.CSSProperties; + posts: HttpTimelinePostInfo[]; + onReload: () => void; +} + +const TimelinePagedPostListView: React.FC = ( + props +) => { + const { className, style, posts, onReload } = props; + + const [lastViewCount, setLastViewCount] = React.useState(10); + + const viewingPosts = React.useMemo(() => { + return lastViewCount >= posts.length + ? posts.slice() + : posts.slice(-lastViewCount); + }, [posts, lastViewCount]); + + useScrollToTop(() => { + setLastViewCount(lastViewCount + 10); + }, lastViewCount < posts.length); + + return ( + + ); +}; + +export default TimelinePagedPostListView; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx new file mode 100644 index 00000000..f1b53335 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx @@ -0,0 +1,197 @@ +import React from "react"; +import classnames from "classnames"; +import { Remarkable } from "remarkable"; + +import { UiLogicError } from "@/common"; + +import { HttpNetworkError } from "http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; + +import { useUser } from "@/services/user"; + +import Skeleton from "../common/Skeleton"; +import LoadFailReload from "../common/LoadFailReload"; + +const TextView: React.FC = (props) => { + const { post, className, style } = props; + + const [text, setText] = React.useState(null); + const [error, setError] = React.useState<"offline" | "error" | null>(null); + + const [reloadKey, setReloadKey] = React.useState(0); + + React.useEffect(() => { + let subscribe = true; + + setText(null); + setError(null); + + void getHttpTimelineClient() + .getPostDataAsString(post.timelineName, post.id) + .then( + (data) => { + if (subscribe) setText(data); + }, + (error) => { + if (subscribe) { + if (error instanceof HttpNetworkError) { + setError("offline"); + } else { + setError("error"); + } + } + } + ); + + return () => { + subscribe = false; + }; + }, [post.timelineName, post.id, reloadKey]); + + if (error != null) { + return ( + setReloadKey(reloadKey + 1)} + /> + ); + } else if (text == null) { + return ; + } else { + return ( +
+ {text} +
+ ); + } +}; + +const ImageView: React.FC = (props) => { + const { post, className, style } = props; + + useUser(); + + return ( + + ); +}; + +const MarkdownView: React.FC = (props) => { + const { post, className, style } = props; + + const _remarkable = React.useRef(); + + const getRemarkable = (): Remarkable => { + if (_remarkable.current) { + return _remarkable.current; + } else { + _remarkable.current = new Remarkable(); + return _remarkable.current; + } + }; + + const [markdown, setMarkdown] = React.useState(null); + const [error, setError] = React.useState<"offline" | "error" | null>(null); + + const [reloadKey, setReloadKey] = React.useState(0); + + React.useEffect(() => { + let subscribe = true; + + setMarkdown(null); + setError(null); + + void getHttpTimelineClient() + .getPostDataAsString(post.timelineName, post.id) + .then( + (data) => { + if (subscribe) setMarkdown(data); + }, + (error) => { + if (subscribe) { + if (error instanceof HttpNetworkError) { + setError("offline"); + } else { + setError("error"); + } + } + } + ); + + return () => { + subscribe = false; + }; + }, [post.timelineName, post.id, reloadKey]); + + const markdownHtml = React.useMemo(() => { + if (markdown == null) return null; + return getRemarkable().render(markdown); + }, [markdown]); + + if (error != null) { + return ( + setReloadKey(reloadKey + 1)} + /> + ); + } else if (markdown == null) { + return ; + } else { + if (markdownHtml == null) { + throw new UiLogicError("Markdown is not null but markdown html is."); + } + return ( +
+ ); + } +}; + +export interface TimelinePostContentViewProps { + post: HttpTimelinePostInfo; + className?: string; + style?: React.CSSProperties; +} + +const viewMap: Record> = { + "text/plain": TextView, + "text/markdown": MarkdownView, + "image/png": ImageView, + "image/jpeg": ImageView, + "image/gif": ImageView, + "image/webp": ImageView, +}; + +const TimelinePostContentView: React.FC = ( + props +) => { + const { post, className, style } = props; + + const type = post.dataList[0].kind; + + if (type in viewMap) { + const View = viewMap[type]; + return ; + } else { + // TODO: i18n + console.error("Unknown post type", post); + return
Error, unknown post type!
; + } +}; + +export default TimelinePostContentView; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx new file mode 100644 index 00000000..b2c7a470 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { Modal, Button } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +const TimelinePostDeleteConfirmDialog: React.FC<{ + onClose: () => void; + onConfirm: () => void; +}> = ({ onClose, onConfirm }) => { + const { t } = useTranslation(); + + return ( + + + + {t("timeline.post.deleteDialog.title")} + + + {t("timeline.post.deleteDialog.prompt")} + + + + + + ); +}; + +export default TimelinePostDeleteConfirmDialog; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx new file mode 100644 index 00000000..0f470fd6 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -0,0 +1,291 @@ +import React from "react"; +import classnames from "classnames"; +import { useTranslation } from "react-i18next"; +import { Row, Col, Form } from "react-bootstrap"; + +import { UiLogicError } from "@/common"; + +import { + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePostInfo, + HttpTimelinePostPostRequestData, +} from "http/timeline"; + +import { pushAlert } from "@/services/alert"; +import { base64 } from "http/common"; + +import BlobImage from "../common/BlobImage"; +import LoadingButton from "../common/LoadingButton"; +import { PopupMenu } from "../common/Menu"; +import MarkdownPostEdit from "./MarkdownPostEdit"; + +interface TimelinePostEditTextProps { + text: string; + disabled: boolean; + onChange: (text: string) => void; + className?: string; + style?: React.CSSProperties; +} + +const TimelinePostEditText: React.FC = (props) => { + const { text, disabled, onChange, className, style } = props; + + return ( + { + onChange(event.target.value); + }} + className={className} + style={style} + /> + ); +}; + +interface TimelinePostEditImageProps { + onSelect: (file: File | null) => void; + disabled: boolean; +} + +const TimelinePostEditImage: React.FC = (props) => { + const { onSelect, disabled } = props; + + const { t } = useTranslation(); + + const [file, setFile] = React.useState(null); + const [error, setError] = React.useState(false); + + const onInputChange: React.ChangeEventHandler = (e) => { + setError(false); + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + onSelect(null); + } else { + setFile(files[0]); + } + }; + + React.useEffect(() => { + return () => { + onSelect(null); + }; + }, [onSelect]); + + return ( + <> + + {file != null && !error && ( + onSelect(file)} + onError={() => { + onSelect(null); + setError(true); + }} + /> + )} + {error ?
{t("loadImageError")}
: null} + + ); +}; + +type PostKind = "text" | "markdown" | "image"; + +const postKindIconClassNameMap: Record = { + text: "bi-fonts", + markdown: "bi-markdown", + image: "bi-image", +}; + +export interface TimelinePostEditProps { + className?: string; + timeline: HttpTimelineInfo; + onPosted: (newPost: HttpTimelinePostInfo) => void; + onHeightChange?: (height: number) => void; +} + +const TimelinePostEdit: React.FC = (props) => { + const { timeline, onHeightChange, className, onPosted } = props; + + const { t } = useTranslation(); + + const [process, setProcess] = React.useState(false); + + const [kind, setKind] = React.useState>("text"); + const [showMarkdown, setShowMarkdown] = React.useState(false); + + const [text, setText] = React.useState(""); + const [image, setImage] = React.useState(null); + + const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`; + + React.useEffect(() => { + setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); + }, [draftTextLocalStorageKey]); + + const canSend = + (kind === "text" && text.length !== 0) || + (kind === "image" && image != null); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const containerRef = React.useRef(null!); + + const notifyHeightChange = (): void => { + if (onHeightChange) { + onHeightChange(containerRef.current.clientHeight); + } + }; + + React.useEffect(() => { + notifyHeightChange(); + return () => { + if (onHeightChange) { + onHeightChange(0); + } + }; + }); + + const onPostError = (): void => { + pushAlert({ + type: "danger", + message: "timeline.sendPostFailed", + }); + }; + + const onSend = async (): Promise => { + setProcess(true); + + let requestData: HttpTimelinePostPostRequestData; + switch (kind) { + case "text": + requestData = { + contentType: "text/plain", + data: await base64(text), + }; + break; + case "image": + if (image == null) { + throw new UiLogicError( + "Content type is image but image blob is null." + ); + } + requestData = { + contentType: image.type, + data: await base64(image), + }; + break; + default: + throw new UiLogicError("Unknown content type."); + } + + getHttpTimelineClient() + .postPost(timeline.name, { + dataList: [requestData], + }) + .then( + (data) => { + if (kind === "text") { + setText(""); + window.localStorage.removeItem(draftTextLocalStorageKey); + } + setProcess(false); + setKind("text"); + onPosted(data); + }, + (_) => { + setProcess(false); + onPostError(); + } + ); + }; + + return ( +
+ {showMarkdown ? ( + setShowMarkdown(false)} + timeline={timeline.name} + onPosted={onPosted} + onPostError={onPostError} + /> + ) : ( + + + {(() => { + if (kind === "text") { + return ( + { + setText(t); + window.localStorage.setItem(draftTextLocalStorageKey, t); + }} + /> + ); + } else if (kind === "image") { + return ( + + ); + } + })()} + + +
+ ({ + type: "button", + text: `timeline.post.type.${kind}`, + iconClassName: postKindIconClassNameMap[kind], + onClick: () => { + if (kind === "markdown") { + setShowMarkdown(true); + } else { + setKind(kind); + } + }, + }))} + > + + +
+ + {t("timeline.send")} + + +
+ )} +
+ ); +}; + +export default TimelinePostEdit; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx new file mode 100644 index 00000000..49284720 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx @@ -0,0 +1,79 @@ +import React, { Fragment } from "react"; +import classnames from "classnames"; + +import { HttpTimelinePostInfo } from "http/timeline"; + +import TimelinePostView from "./TimelinePostView"; +import TimelineDateLabel from "./TimelineDateLabel"; + +function dateEqual(left: Date, right: Date): boolean { + return ( + left.getDate() == right.getDate() && + left.getMonth() == right.getMonth() && + left.getFullYear() == right.getFullYear() + ); +} + +export interface TimelinePostListViewProps { + className?: string; + style?: React.CSSProperties; + posts: HttpTimelinePostInfo[]; + onReload: () => void; +} + +const TimelinePostListView: React.FC = (props) => { + const { className, style, posts, onReload } = props; + + const groupedPosts = React.useMemo< + { + date: Date; + posts: (HttpTimelinePostInfo & { index: number })[]; + }[] + >(() => { + const result: { + date: Date; + posts: (HttpTimelinePostInfo & { index: number })[]; + }[] = []; + let index = 0; + for (const post of posts) { + const time = new Date(post.time); + if (result.length === 0) { + result.push({ date: time, posts: [{ ...post, index }] }); + } else { + const lastGroup = result[result.length - 1]; + if (dateEqual(lastGroup.date, time)) { + lastGroup.posts.push({ ...post, index }); + } else { + result.push({ date: time, posts: [{ ...post, index }] }); + } + } + index++; + } + return result; + }, [posts]); + + return ( +
+ {groupedPosts.map((group) => { + return ( + + + {group.posts.map((post) => { + return ( + + ); + })} + + ); + })} +
+ ); +}; + +export default TimelinePostListView; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx new file mode 100644 index 00000000..e8b32c71 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import classnames from "classnames"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; + +import { pushAlert } from "@/services/alert"; + +import UserAvatar from "../common/user/UserAvatar"; +import TimelineLine from "./TimelineLine"; +import TimelinePostContentView from "./TimelinePostContentView"; +import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; +import PostPropertyChangeDialog from "./PostPropertyChangeDialog"; + +export interface TimelinePostViewProps { + post: HttpTimelinePostInfo; + current?: boolean; + 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 current = props.current === true; + + const { t } = useTranslation(); + + const [operationMaskVisible, setOperationMaskVisible] = + React.useState(false); + const [dialog, setDialog] = React.useState< + "delete" | "changeproperty" | null + >(null); + + 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(); + }} + > + {t("changeProperty")} + + { + setDialog("delete"); + e.stopPropagation(); + }} + > + {t("delete")} + +
+ ) : null} +
+ {dialog === "delete" ? ( + { + setDialog(null); + setOperationMaskVisible(false); + }} + onConfirm={() => { + void getHttpTimelineClient() + .deletePost(post.timelineName, post.id) + .then(onDeleted, () => { + pushAlert({ + type: "danger", + message: "timeline.deletePostFailed", + }); + }); + }} + /> + ) : dialog === "changeproperty" ? ( + { + setDialog(null); + setOperationMaskVisible(false); + }} + post={post} + onSuccess={onChanged} + /> + ) : null} +
+ ); +}; + +export default TimelinePostView; diff --git a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx new file mode 100644 index 00000000..83b24d01 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -0,0 +1,87 @@ +import React from "react"; + +import { + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePatchRequest, + kTimelineVisibilities, + TimelineVisibility, +} from "http/timeline"; + +import OperationDialog from "../common/OperationDialog"; + +export interface TimelinePropertyChangeDialogProps { + open: boolean; + close: () => void; + timeline: HttpTimelineInfo; + onChange: () => void; +} + +const labelMap: { [key in TimelineVisibility]: string } = { + Private: "timeline.visibility.private", + Public: "timeline.visibility.public", + Register: "timeline.visibility.register", +}; + +const TimelinePropertyChangeDialog: React.FC = + (props) => { + const { timeline, onChange } = props; + + return ( + ({ + label: labelMap[v], + value: v, + })), + initValue: timeline.visibility, + }, + { + type: "text", + label: "timeline.dialogChangeProperty.description", + initValue: timeline.description, + }, + { + type: "color", + label: "timeline.dialogChangeProperty.color", + initValue: timeline.color ?? null, + canBeNull: true, + }, + ] as const + } + open={props.open} + close={props.close} + onProcess={([newTitle, newVisibility, newDescription, newColor]) => { + const req: HttpTimelinePatchRequest = {}; + if (newTitle !== timeline.title) { + req.title = newTitle; + } + if (newVisibility !== timeline.visibility) { + req.visibility = newVisibility as TimelineVisibility; + } + if (newDescription !== timeline.description) { + req.description = newDescription; + } + const nc = newColor ?? ""; + if (nc !== timeline.color) { + req.color = nc; + } + return getHttpTimelineClient() + .patchTimeline(timeline.name, req) + .then(onChange); + }} + /> + ); + }; + +export default TimelinePropertyChangeDialog; diff --git a/FrontEnd/src/views/timeline-common/TimelineTop.tsx b/FrontEnd/src/views/timeline-common/TimelineTop.tsx new file mode 100644 index 00000000..dabbdf1e --- /dev/null +++ b/FrontEnd/src/views/timeline-common/TimelineTop.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import classnames from "classnames"; + +import TimelineLine, { TimelineLineProps } from "./TimelineLine"; + +export interface TimelineTopProps { + height?: number | string; + lineProps?: TimelineLineProps; + className?: string; + style?: React.CSSProperties; +} + +const TimelineTop: React.FC = (props) => { + const { height, style, className } = props; + const lineProps = props.lineProps ?? { center: "none" }; + + return ( +
+ +
+ ); +}; + +export default TimelineTop; diff --git a/FrontEnd/src/views/timeline-common/timeline-common.sass b/FrontEnd/src/views/timeline-common/timeline-common.sass new file mode 100644 index 00000000..4400fead --- /dev/null +++ b/FrontEnd/src/views/timeline-common/timeline-common.sass @@ -0,0 +1,259 @@ +@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 -- cgit v1.2.3 From e0b766203d7576ab67b16ba556ba14120d0bc876 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 15:02:49 +0800 Subject: ... --- BackEnd/Timeline/FrontEndMode.cs | 1 - BackEnd/Timeline/Properties/launchSettings.json | 2 +- BackEnd/Timeline/Startup.cs | 4 --- FrontEnd/index.html | 29 ++++++++++++++++++++++ FrontEnd/package.json | 5 ++-- FrontEnd/postcss.config.js | 11 +++----- FrontEnd/src/index.ejs | 29 ---------------------- FrontEnd/src/services/TimelinePostBuilder.ts | 4 +-- FrontEnd/src/services/timeline.ts | 4 +-- FrontEnd/src/services/user.ts | 6 ++--- FrontEnd/src/tsconfig.json | 23 ----------------- FrontEnd/src/views/admin/UserAdmin.tsx | 2 +- FrontEnd/src/views/center/CenterBoards.tsx | 6 ++--- FrontEnd/src/views/center/TimelineBoard.tsx | 4 +-- FrontEnd/src/views/center/TimelineCreateDialog.tsx | 2 +- FrontEnd/src/views/common/user/UserAvatar.tsx | 2 +- FrontEnd/src/views/home/TimelineListView.tsx | 4 +-- FrontEnd/src/views/home/index.tsx | 4 +-- FrontEnd/src/views/search/index.tsx | 10 ++++---- FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 2 +- .../src/views/settings/ChangeNicknameDialog.tsx | 2 +- .../timeline-common/ConnectionStatusBadge.tsx | 2 +- .../src/views/timeline-common/MarkdownPostEdit.tsx | 2 +- .../timeline-common/PostPropertyChangeDialog.tsx | 2 +- FrontEnd/src/views/timeline-common/Timeline.tsx | 6 ++--- .../src/views/timeline-common/TimelineMember.tsx | 8 +++--- .../timeline-common/TimelinePageCardTemplate.tsx | 4 +-- .../views/timeline-common/TimelinePageTemplate.tsx | 4 +-- .../timeline-common/TimelinePagedPostListView.tsx | 2 +- .../timeline-common/TimelinePostContentView.tsx | 4 +-- .../src/views/timeline-common/TimelinePostEdit.tsx | 6 ++--- .../views/timeline-common/TimelinePostListView.tsx | 2 +- .../src/views/timeline-common/TimelinePostView.tsx | 2 +- .../TimelinePropertyChangeDialog.tsx | 2 +- FrontEnd/src/views/timeline/TimelineCard.tsx | 2 +- .../src/views/timeline/TimelineDeleteDialog.tsx | 2 +- FrontEnd/src/views/user/index.tsx | 2 +- FrontEnd/tsconfig.json | 25 +++++++++++++++++++ FrontEnd/vite.config.js | 3 +++ 39 files changed, 116 insertions(+), 120 deletions(-) create mode 100644 FrontEnd/index.html delete mode 100644 FrontEnd/src/index.ejs delete mode 100644 FrontEnd/src/tsconfig.json create mode 100644 FrontEnd/tsconfig.json (limited to 'FrontEnd/src/views/timeline-common') diff --git a/BackEnd/Timeline/FrontEndMode.cs b/BackEnd/Timeline/FrontEndMode.cs index 63503292..eb718028 100644 --- a/BackEnd/Timeline/FrontEndMode.cs +++ b/BackEnd/Timeline/FrontEndMode.cs @@ -4,7 +4,6 @@ { Disable, Mock, - Proxy, Normal } } diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json index f683ca2d..7f68b709 100644 --- a/BackEnd/Timeline/Properties/launchSettings.json +++ b/BackEnd/Timeline/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_FRONTEND": "Proxy" + "ASPNETCORE_FRONTEND": "Disable" } }, "Dev-Mock": { diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 18097e2c..26ffb4b1 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -166,10 +166,6 @@ namespace Timeline { app.UseSpa(spa => { - if (_frontEndMode == FrontEndMode.Proxy) - { - spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri); - } }); } } diff --git a/FrontEnd/index.html b/FrontEnd/index.html new file mode 100644 index 00000000..87e19743 --- /dev/null +++ b/FrontEnd/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + Timeline + + + +
+ + + diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 1afb5ad0..5d12899e 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -33,8 +33,9 @@ "xregexp": "^5.0.2" }, "scripts": { - "start": "webpack serve --config ./webpack.config.dev.js", - "build": "webpack --config ./webpack.config.prod.js", + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx", "lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix" }, diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js index 74ee8155..9129aa1f 100644 --- a/FrontEnd/postcss.config.js +++ b/FrontEnd/postcss.config.js @@ -1,10 +1,5 @@ +import postcssPresetEnv from "postcss-preset-env"; + module.exports = { - plugins: [ - [ - "postcss-preset-env", - { - // Options - }, - ], - ], + plugins: [postcssPresetEnv()], }; diff --git a/FrontEnd/src/index.ejs b/FrontEnd/src/index.ejs deleted file mode 100644 index c2ff4182..00000000 --- a/FrontEnd/src/index.ejs +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - - diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts index fe4c7a9a..40279eca 100644 --- a/FrontEnd/src/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/services/TimelinePostBuilder.ts @@ -2,8 +2,8 @@ import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; -import { base64 } from "http/common"; -import { HttpTimelinePostPostRequest } from "http/timeline"; +import { base64 } from "@/http/common"; +import { HttpTimelinePostPostRequest } from "@/http/timeline"; export default class TimelinePostBuilder { private _onChange: () => void; diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts index 4ebb705d..d8c0ae00 100644 --- a/FrontEnd/src/services/timeline.ts +++ b/FrontEnd/src/services/timeline.ts @@ -1,9 +1,9 @@ -import { TimelineVisibility } from "http/timeline"; +import { TimelineVisibility } from "@/http/timeline"; import XRegExp from "xregexp"; import { Observable } from "rxjs"; import { HubConnectionBuilder, HubConnectionState } from "@microsoft/signalr"; -import { getHttpToken } from "http/common"; +import { getHttpToken } from "@/http/common"; const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index 3375c88a..9a8e5687 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -3,12 +3,12 @@ import { BehaviorSubject, Observable } from "rxjs"; import { UiLogicError } from "@/common"; -import { HttpNetworkError, setHttpToken } from "http/common"; +import { HttpNetworkError, setHttpToken } from "@/http/common"; import { getHttpTokenClient, HttpCreateTokenBadCredentialError, -} from "http/token"; -import { getHttpUserClient, HttpUser, UserPermission } from "http/user"; +} from "@/http/token"; +import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user"; import { pushAlert } from "./alert"; diff --git a/FrontEnd/src/tsconfig.json b/FrontEnd/src/tsconfig.json deleted file mode 100644 index 817c50bd..00000000 --- a/FrontEnd/src/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react", - "types": ["vite/client"], - "sourceMap": true, - "baseUrl": "./", - "paths": { - "@/*": ["*"] - }, - "lib": ["dom", "dom.iterable", "esnext"] - }, - "include": ["."] -} diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 4e9cd600..558d3aee 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -12,7 +12,7 @@ import { HttpUser, kUserPermissionList, UserPermission, -} from "http/user"; +} from "@/http/user"; import { Trans, useTranslation } from "react-i18next"; interface DialogProps { diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx index 431d1e9a..f5200415 100644 --- a/FrontEnd/src/views/center/CenterBoards.tsx +++ b/FrontEnd/src/views/center/CenterBoards.tsx @@ -5,9 +5,9 @@ import { useTranslation } from "react-i18next"; import { pushAlert } from "@/services/alert"; import { useUserLoggedIn } from "@/services/user"; -import { getHttpTimelineClient } from "http/timeline"; -import { getHttpBookmarkClient } from "http/bookmark"; -import { getHttpHighlightClient } from "http/highlight"; +import { getHttpTimelineClient } from "@/http/timeline"; +import { getHttpBookmarkClient } from "@/http/bookmark"; +import { getHttpHighlightClient } from "@/http/highlight"; import TimelineBoard from "./TimelineBoard"; diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index bb80266b..840c0415 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Spinner } from "react-bootstrap"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; @@ -34,7 +34,7 @@ const TimelineBoardItem: React.FC = ({ actions, }) => { const { name, title } = timeline; - const isPersonal = name.startsWith("@"); + const isPersonal = name.startsWith("src"); const url = isPersonal ? `/users/${timeline.owner.username}` : `/timelines/${name}`; diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx index a2437ae5..b4e25ba1 100644 --- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx @@ -3,7 +3,7 @@ import { useHistory } from "react-router"; import { validateTimelineName } from "@/services/timeline"; import OperationDialog from "../common/OperationDialog"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; interface TimelineCreateDialogProps { open: boolean; diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx index 901697db..9e822528 100644 --- a/FrontEnd/src/views/common/user/UserAvatar.tsx +++ b/FrontEnd/src/views/common/user/UserAvatar.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; export interface UserAvatarProps extends React.ImgHTMLAttributes { diff --git a/FrontEnd/src/views/home/TimelineListView.tsx b/FrontEnd/src/views/home/TimelineListView.tsx index 975875af..2fb54820 100644 --- a/FrontEnd/src/views/home/TimelineListView.tsx +++ b/FrontEnd/src/views/home/TimelineListView.tsx @@ -2,7 +2,7 @@ import React from "react"; import { convertI18nText, I18nText } from "@/common"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -13,7 +13,7 @@ interface TimelineListItemProps { const TimelineListItem: React.FC = ({ timeline }) => { const url = React.useMemo( () => - timeline.name.startsWith("@") + timeline.name.startsWith("src") ? `/users/${timeline.owner.username}` : `/timelines/${timeline.name}`, [timeline] diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx index efc364d7..0eca23ee 100644 --- a/FrontEnd/src/views/home/index.tsx +++ b/FrontEnd/src/views/home/index.tsx @@ -1,8 +1,8 @@ import React from "react"; import { useHistory } from "react-router"; -import { HttpTimelineInfo } from "http/timeline"; -import { getHttpHighlightClient } from "http/highlight"; +import { HttpTimelineInfo } from "@/http/timeline"; +import { getHttpHighlightClient } from "@/http/highlight"; import SearchInput from "../common/SearchInput"; import TimelineListView from "./TimelineListView"; diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx index 14a9709c..9a26802d 100644 --- a/FrontEnd/src/views/search/index.tsx +++ b/FrontEnd/src/views/search/index.tsx @@ -4,9 +4,9 @@ import { Container, Row } from "react-bootstrap"; import { useHistory, useLocation } from "react-router"; import { Link } from "react-router-dom"; -import { HttpNetworkError } from "http/common"; -import { getHttpSearchClient } from "http/search"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpNetworkError } from "@/http/common"; +import { getHttpSearchClient } from "@/http/search"; +import { HttpTimelineInfo } from "@/http/timeline"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; @@ -14,7 +14,7 @@ import UserAvatar from "../common/user/UserAvatar"; const TimelineSearchResultItemView: React.FC<{ timeline: HttpTimelineInfo; }> = ({ timeline }) => { - const link = timeline.name.startsWith("@") + const link = timeline.name.startsWith("src") ? `users/${timeline.owner.username}` : `timelines/${timeline.name}`; @@ -33,7 +33,7 @@ const TimelineSearchResultItemView: React.FC<{ /> {timeline.owner.nickname} - @{timeline.owner.username} + src{timeline.owner.username}
diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx index 338d2112..c4f6f492 100644 --- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx @@ -7,7 +7,7 @@ import { UiLogicError } from "@/common"; import { useUserLoggedIn } from "@/services/user"; -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx index e6420f36..4b44cdd6 100644 --- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx @@ -1,4 +1,4 @@ -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; import { useUserLoggedIn } from "@/services/user"; import React from "react"; diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx index df43d8d2..1b9d6d2a 100644 --- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx +++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx @@ -1,6 +1,6 @@ import React from "react"; import classnames from "classnames"; -import { HubConnectionState } from "@microsoft/signalr"; +import { HubConnectionState } from "srcmicrosoft/signalr"; import { useTranslation } from "react-i18next"; export interface ConnectionStatusBadgeProps { diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 1514d28f..685e17be 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -4,7 +4,7 @@ import { Form, Spinner } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Prompt } from "react-router"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import FlatButton from "../common/FlatButton"; import TabPages from "../common/TabPages"; diff --git a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx index 21c5272e..001e52d7 100644 --- a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx index 40619e64..31ea5870 100644 --- a/FrontEnd/src/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/views/timeline-common/Timeline.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { HubConnectionState } from "@microsoft/signalr"; +import { HubConnectionState } from "srcmicrosoft/signalr"; import { HttpForbiddenError, HttpNetworkError, HttpNotFoundError, -} from "http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +} from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { getTimelinePostUpdate$ } from "@/services/timeline"; diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx index 3d4de8b8..1ef085a7 100644 --- a/FrontEnd/src/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx @@ -4,12 +4,12 @@ import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; import { convertI18nText, I18nText } from "@/common"; -import { HttpUser } from "http/user"; -import { getHttpSearchClient } from "http/search"; +import { HttpUser } from "@/http/user"; +import { getHttpSearchClient } from "@/http/search"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; const TimelineMemberItem: React.FC<{ user: HttpUser; @@ -27,7 +27,7 @@ const TimelineMemberItem: React.FC<{ {user.nickname} - {"@" + user.username} + {"src" + user.username} {onAction ? ( diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx index 038ea3ab..623d643f 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -2,8 +2,8 @@ import React from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { getHttpHighlightClient } from "http/highlight"; -import { getHttpBookmarkClient } from "http/bookmark"; +import { getHttpHighlightClient } from "@/http/highlight"; +import { getHttpBookmarkClient } from "@/http/bookmark"; import { useUser } from "@/services/user"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx index 44926cc6..658ce502 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx @@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next"; import { Container } from "react-bootstrap"; import { HubConnectionState } from "@microsoft/signalr"; -import { HttpNetworkError, HttpNotFoundError } from "http/common"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import { getAlertHost } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx index d569a2d7..37f02a82 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { HttpTimelinePostInfo } from "http/timeline"; +import { HttpTimelinePostInfo } from "@/http/timeline"; import useScrollToTop from "@/utilities/useScrollToTop"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx index f1b53335..607b72c9 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx @@ -4,8 +4,8 @@ import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; -import { HttpNetworkError } from "http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { HttpNetworkError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { useUser } from "@/services/user"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx index 0f470fd6..1f9f02a5 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -10,10 +10,10 @@ import { HttpTimelineInfo, HttpTimelinePostInfo, HttpTimelinePostPostRequestData, -} from "http/timeline"; +} from "@/http/timeline"; import { pushAlert } from "@/services/alert"; -import { base64 } from "http/common"; +import { base64 } from "@/http/common"; import BlobImage from "../common/BlobImage"; import LoadingButton from "../common/LoadingButton"; @@ -138,7 +138,7 @@ const TimelinePostEdit: React.FC = (props) => { (kind === "text" && text.length !== 0) || (kind === "image" && image != null); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line srctypescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); const notifyHeightChange = (): void => { diff --git a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx index 49284720..ba204b72 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from "react"; import classnames from "classnames"; -import { HttpTimelinePostInfo } from "http/timeline"; +import { HttpTimelinePostInfo } from "@/http/timeline"; import TimelinePostView from "./TimelinePostView"; import TimelineDateLabel from "./TimelineDateLabel"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index e8b32c71..f7b81478 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx index 83b24d01..70f72025 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -6,7 +6,7 @@ import { HttpTimelinePatchRequest, kTimelineVisibilities, TimelineVisibility, -} from "http/timeline"; +} from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx index e031b565..86063843 100644 --- a/FrontEnd/src/views/timeline/TimelineCard.tsx +++ b/FrontEnd/src/views/timeline/TimelineCard.tsx @@ -29,7 +29,7 @@ const TimelineCard: React.FC = (props) => { /> {timeline.owner.nickname} - @{timeline.owner.username} + src{timeline.owner.username}
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx index 8821507d..dbca62ca 100644 --- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useHistory } from "react-router"; import { Trans } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/user/index.tsx b/FrontEnd/src/views/user/index.tsx index 57454d0d..0013b254 100644 --- a/FrontEnd/src/views/user/index.tsx +++ b/FrontEnd/src/views/user/index.tsx @@ -14,7 +14,7 @@ const UserPage: React.FC = () => { return ( <> setReloadKey(reloadKey + 1)} diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json new file mode 100644 index 00000000..3afe2c3e --- /dev/null +++ b/FrontEnd/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "noEmit": true, + "types": ["vite/client"], + "sourceMap": true, + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["./src"] +} diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js index 6e8cde1b..2e85c36a 100644 --- a/FrontEnd/vite.config.js +++ b/FrontEnd/vite.config.js @@ -7,6 +7,9 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [reactRefresh()], + resolve: { + alias: [{ find: "@", replacement: "/src" }], + }, server: { port: 13000, proxy: { -- cgit v1.2.3 From ddce03a67708249eec129eb36744be460345bd75 Mon Sep 17 00:00:00 2001 From: crupest 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/timeline-common') 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 = (props) => { {title != null &&

{title}

} {editable && (editing ? ( -
{ setEditing(false); }} - > - {t("done")} -
+ /> ) : ( -
{ setEditing(true); }} - > - {t("edit")} -
+ /> ))}
{(() => { 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 = (props) => { - const { disabled, className, style } = props; - const variant = props.variant ?? "primary"; - - const onClick = disabled ? undefined : props.onClick; - - return ( -
- {props.children} -
- ); -}; - -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 +): React.ReactElement | null { + const { t } = useTranslation(); + + return ( + + ); +} + +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 From 57a4aa9bc47d3d38f66819f01f41ab10e9673667 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 16:46:12 +0800 Subject: ... --- FrontEnd/src/views/common/button/FlatButton.tsx | 7 ++++++- FrontEnd/src/views/common/button/TextButton.tsx | 7 ++++++- FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx | 13 +++++-------- 3 files changed, 17 insertions(+), 10 deletions(-) (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 24f47785..f5349765 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -12,10 +12,14 @@ function _FlatButton( text, color, onClick, + className, + style, }: { text: I18nText; color?: PaletteColorType; onClick?: () => void; + className?: string; + style?: React.CSSProperties; }, ref: React.ForwardedRef ): React.ReactElement | null { @@ -24,8 +28,9 @@ function _FlatButton( return ( diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx index 2014158a..1e2b4873 100644 --- a/FrontEnd/src/views/common/button/TextButton.tsx +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -12,10 +12,14 @@ function _TextButton( text, color, onClick, + className, + style, }: { text: I18nText; color?: PaletteColorType; onClick?: () => void; + className?: string; + style?: React.CSSProperties; }, ref: React.ForwardedRef ): React.ReactElement | null { @@ -24,8 +28,9 @@ function _TextButton( return ( diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 685e17be..005da933 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -6,7 +6,7 @@ import { Prompt } from "react-router"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; -import FlatButton from "../common/FlatButton"; +import FlatButton from "../common/button/FlatButton"; import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; import ConfirmDialog from "../common/ConfirmDialog"; @@ -106,8 +106,9 @@ const MarkdownPostEdit: React.FC = ({ ) : ( <> { if (canLeave) { onClose(); @@ -115,12 +116,8 @@ const MarkdownPostEdit: React.FC = ({ setShowLeaveConfirmDialog(true); } }} - > - {t("operationDialog.cancel")} - - - {t("timeline.send")} - + /> + {canSend && } ) } -- cgit v1.2.3 From 1f242271a98900ca0a72a13ab05efbf65090df0d Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 17:04:25 +0800 Subject: ... --- FrontEnd/src/index.css | 10 ---------- FrontEnd/src/views/about/index.tsx | 14 ++++++++------ FrontEnd/src/views/center/TimelineBoard.tsx | 5 +++-- FrontEnd/src/views/common/Card.css | 11 +++++++++++ FrontEnd/src/views/common/Card.tsx | 22 ++++++++++++++++++++++ FrontEnd/src/views/settings/index.tsx | 9 +++++---- .../timeline-common/TimelinePageCardTemplate.tsx | 7 ++++--- 7 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 FrontEnd/src/views/common/Card.css create mode 100644 FrontEnd/src/views/common/Card.tsx (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index ff8c1866..8d428774 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -53,16 +53,6 @@ textarea { 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; diff --git a/FrontEnd/src/views/about/index.tsx b/FrontEnd/src/views/about/index.tsx index db4814c4..7b0e50b0 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 Card from "../common/Card"; + import "./index.css"; const frontendCredits: { @@ -68,7 +70,7 @@ const AboutPage: React.FC = () => { return (
-
+

{t("about.author.title")}

@@ -102,8 +104,8 @@ const AboutPage: React.FC = () => {

-
-
+ +

{t("about.site.title")}

@@ -120,8 +122,8 @@ const AboutPage: React.FC = () => { {t("about.site.repo")}

-
-
+ +

{t("about.credits.title")}

{t("about.credits.content")}

{t("about.credits.frontend")}

@@ -150,7 +152,7 @@ const AboutPage: React.FC = () => { })}
  • ...
  • -
    +
    ); }; diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index e0a2d80e..7b9981e5 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -9,6 +9,7 @@ import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; import FlatButton from "../common/button/FlatButton"; +import Card from "../common/Card"; interface TimelineBoardItemProps { timeline: HttpTimelineInfo; @@ -224,7 +225,7 @@ const TimelineBoardUI: React.FC = (props) => { const [editing, setEditing] = React.useState(false); return ( -
    +
    {title != null &&

    {title}

    } {editable && @@ -280,7 +281,7 @@ const TimelineBoardUI: React.FC = (props) => { ); } })()} -
    +
    ); }; diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css new file mode 100644 index 00000000..daf4e12b --- /dev/null +++ b/FrontEnd/src/views/common/Card.css @@ -0,0 +1,11 @@ +.cru-card { + border: 1px solid; + border-color: #e9ecef; + border-radius: 8px; + background: #f8f9fa; + transition: all 0.3s; +} + +.cru-card:hover { + border-color: var(--tl-primary-color); +} diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx new file mode 100644 index 00000000..da2a1b68 --- /dev/null +++ b/FrontEnd/src/views/common/Card.tsx @@ -0,0 +1,22 @@ +import classNames from "classnames"; +import React from "react"; + +import "./Card.css"; + +function _Card( + { + className, + children, + }: React.PropsWithChildren>, + ref: React.ForwardedRef +): React.ReactElement | null { + return ( +
    + {children} +
    + ); +} + +const Card = React.forwardRef(_Card); + +export default Card; diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx index f0bed222..840bb7e8 100644 --- a/FrontEnd/src/views/settings/index.tsx +++ b/FrontEnd/src/views/settings/index.tsx @@ -8,6 +8,7 @@ import { useUser, userService } from "@/services/user"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; +import Card from "../common/Card"; import "./index.css"; @@ -52,7 +53,7 @@ const SettingsPage: React.FC = (_) => { <> {user ? ( -
    +

    {t("settings.subheaders.account")}

    @@ -82,9 +83,9 @@ const SettingsPage: React.FC = (_) => { > {t("settings.logout")}
    -
    + ) : null} -
    +

    {t("settings.subheaders.customization")}

    @@ -108,7 +109,7 @@ const SettingsPage: React.FC = (_) => { -
    + {(() => { switch (dialog) { diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx index 623d643f..851dfa55 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -19,6 +19,7 @@ import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; import ConnectionStatusBadge from "./ConnectionStatusBadge"; import { MenuItems, PopupMenu } from "../common/Menu"; import FullPage from "../common/FullPage"; +import Card from "../common/Card"; export interface TimelineCardTemplateProps extends TimelinePageCardProps { infoArea: React.ReactElement; @@ -110,8 +111,8 @@ const TimelinePageCardTemplate: React.FC = ({ return ( <> -
    @@ -129,7 +130,7 @@ const TimelinePageCardTemplate: React.FC = ({ ) : (
    {content}
    )} -
    + {(() => { if (dialog === "member") { return ( -- cgit v1.2.3 From 0be1f595578153765d081cdb4478140da9cb8fc9 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 17:14:04 +0800 Subject: ... --- FrontEnd/src/views/common/Card.css | 2 +- FrontEnd/src/views/common/button/FlatButton.tsx | 2 +- FrontEnd/src/views/common/button/TextButton.tsx | 2 +- .../src/views/timeline-common/TimelinePostView.tsx | 25 +++++++++++----------- FrontEnd/src/views/timeline-common/index.css | 8 ++++++- 5 files changed, 22 insertions(+), 17 deletions(-) (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css index daf4e12b..17c3c779 100644 --- a/FrontEnd/src/views/common/Card.css +++ b/FrontEnd/src/views/common/Card.css @@ -2,7 +2,7 @@ border: 1px solid; border-color: #e9ecef; border-radius: 8px; - background: #f8f9fa; + background: #fefeff; transition: all 0.3s; } diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index f5349765..6351971a 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -17,7 +17,7 @@ function _FlatButton( }: { text: I18nText; color?: PaletteColorType; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; className?: string; style?: React.CSSProperties; }, diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx index 1e2b4873..1a2bac94 100644 --- a/FrontEnd/src/views/common/button/TextButton.tsx +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -17,7 +17,7 @@ function _TextButton( }: { text: I18nText; color?: PaletteColorType; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; className?: string; style?: React.CSSProperties; }, diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index f7b81478..e9dd3443 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -8,6 +8,8 @@ import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { pushAlert } from "@/services/alert"; import UserAvatar from "../common/user/UserAvatar"; +import Card from "../common/Card"; +import FlatButton from "../common/button/FlatButton"; import TimelineLine from "./TimelineLine"; import TimelinePostContentView from "./TimelinePostContentView"; import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; @@ -60,7 +62,7 @@ const TimelinePostView: React.FC = (props) => { style={style} > -
    + {post.editable ? ( = (props) => {
    {operationMaskVisible ? (
    { setOperationMaskVisible(false); }} > - { setDialog("changeproperty"); e.stopPropagation(); }} - > - {t("changeProperty")} - - + { setDialog("delete"); e.stopPropagation(); }} - > - {t("delete")} - + />
    ) : null} -
    + {dialog === "delete" ? ( { diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css index 89399961..f35a86c9 100644 --- a/FrontEnd/src/views/timeline-common/index.css +++ b/FrontEnd/src/views/timeline-common/index.css @@ -154,6 +154,7 @@ animation: 0.6s forwards; opacity: 0; } + @media (max-width: 575.98px) { .timeline-item-card { padding-left: 3em; @@ -205,9 +206,14 @@ max-height: 100px; } -.mask { +.timeline-post-item-options-mask { background: rgba(255, 255, 255, 0.8); z-index: 100; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; } .timeline-sync-state-badge { -- cgit v1.2.3 From a8bc59ae259456b7663af5106b3756b85a5e190b Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 17:16:36 +0800 Subject: ... --- FrontEnd/src/views/center/index.css | 5 +++++ FrontEnd/src/views/timeline-common/TimelinePostView.tsx | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/views/center/index.css b/FrontEnd/src/views/center/index.css index 516aba52..f5bbc63b 100644 --- a/FrontEnd/src/views/center/index.css +++ b/FrontEnd/src/views/center/index.css @@ -2,12 +2,14 @@ min-height: 200px; height: 100%; position: relative; + padding: 1em 0; } .timeline-board-header { display: flex; align-items: center; justify-content: space-between; + padding: 0 1em; } .timeline-board-item { @@ -16,11 +18,14 @@ transition: background 0.3s; display: flex; align-items: center; + padding: 0 1em; } + .timeline-board-item .icon { height: 1.3em; color: black; } + .timeline-board-item:hover { background: #dee2e6; } diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index e9dd3443..ea40f80a 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -1,7 +1,6 @@ import React from "react"; import classnames from "classnames"; import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; @@ -29,8 +28,6 @@ const TimelinePostView: React.FC = (props) => { const { post, className, style, cardStyle, onChanged, onDeleted } = props; const current = props.current === true; - const { t } = useTranslation(); - const [operationMaskVisible, setOperationMaskVisible] = React.useState(false); const [dialog, setDialog] = React.useState< -- cgit v1.2.3 From d80619016375d15b05d0f13e5bf68e0d9581aff6 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 17:19:07 +0800 Subject: ... --- FrontEnd/src/views/timeline-common/index.css | 1 + 1 file changed, 1 insertion(+) (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css index f35a86c9..1cad2fb2 100644 --- a/FrontEnd/src/views/timeline-common/index.css +++ b/FrontEnd/src/views/timeline-common/index.css @@ -234,6 +234,7 @@ .timeline-template-card { position: fixed; + z-index: 1031; top: 56px; right: 0; margin: 0.5em; -- cgit v1.2.3 From 08aed0d21a8e7fd5e225140fa1ee8f0e879841c5 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 18:14:58 +0800 Subject: ... --- FrontEnd/package.json | 11 ++- FrontEnd/src/index.tsx | 1 + FrontEnd/src/service-worker.tsx | 104 +++++++++++++++++++++ FrontEnd/src/service-worker.txt | 104 --------------------- FrontEnd/src/sw.ts | 66 +++++++++++++ .../timeline-common/ConnectionStatusBadge.tsx | 2 +- FrontEnd/tsconfig.json | 5 +- FrontEnd/vite.config.js | 13 ++- 8 files changed, 194 insertions(+), 112 deletions(-) create mode 100644 FrontEnd/src/service-worker.tsx delete mode 100644 FrontEnd/src/service-worker.txt create mode 100644 FrontEnd/src/sw.ts (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 38dd85ff..2ae7cae1 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -30,7 +30,13 @@ "regenerator-runtime": "^0.13.7", "remarkable": "^2.0.1", "rxjs": "^7.1.0", - "xregexp": "^5.0.2" + "xregexp": "^5.0.2", + "workbox-cacheable-response": "^6.1.5", + "workbox-expiration": "^6.1.2", + "workbox-precaching": "^6.1.0", + "workbox-routing": "^6.1.5", + "workbox-strategies": "^6.1.0", + "workbox-window": "^6.1.1" }, "scripts": { "start": "vite", @@ -60,6 +66,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "prettier": "^2.3.1", "typescript": "^4.3.2", - "vite": "^2.3.7" + "vite": "^2.3.7", + "vite-plugin-pwa": "^0.8.1" } } diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx index 83c25792..28034601 100644 --- a/FrontEnd/src/index.tsx +++ b/FrontEnd/src/index.tsx @@ -13,6 +13,7 @@ import "./index.css"; import "./i18n"; import "./palette"; +import "./service-worker"; import App from "./App"; diff --git a/FrontEnd/src/service-worker.tsx b/FrontEnd/src/service-worker.tsx new file mode 100644 index 00000000..ea8dfc32 --- /dev/null +++ b/FrontEnd/src/service-worker.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "react-bootstrap"; + +import { pushAlert } from "./services/alert"; + +if ("serviceWorker" in navigator) { + let isThisTriggerUpgrade = false; + + const upgradeSuccessLocalStorageKey = "TIMELINE_UPGRADE_SUCCESS"; + + if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) { + pushAlert({ + message: "serviceWorker.upgradeSuccess", + type: "success", + }); + window.localStorage.removeItem(upgradeSuccessLocalStorageKey); + } + + void import("workbox-window").then(({ Workbox, messageSW }) => { + const wb = new Workbox("/sw.js"); + let registration: ServiceWorkerRegistration | undefined; + + // externalactivated is not usable but I still use its name. + wb.addEventListener("controlling", () => { + const upgradeReload = (): void => { + window.localStorage.setItem(upgradeSuccessLocalStorageKey, "true"); + window.location.reload(); + }; + + if (isThisTriggerUpgrade) { + upgradeReload(); + } else { + const Message: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t("serviceWorker.externalActivatedPrompt")} + + + ); + }; + + pushAlert({ + message: Message, + dismissTime: "never", + type: "warning", + }); + } + }); + + wb.addEventListener("activated", (event) => { + if (!event.isUpdate) { + pushAlert({ + message: "serviceWorker.availableOffline", + type: "success", + }); + } + }); + + // Add an event listener to detect when the registered + // service worker has installed but is waiting to activate. + wb.addEventListener("waiting", (): void => { + const upgrade = (): void => { + isThisTriggerUpgrade = true; + if (registration && registration.waiting) { + // Send a message to the waiting service worker, + // instructing it to activate. + // Note: for this to work, you have to add a message + // listener in your service worker. See below. + void messageSW(registration.waiting, { type: "SKIP_WAITING" }); + } + }; + + const UpgradeMessage: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t("serviceWorker.upgradePrompt")} + + + ); + }; + + pushAlert({ + message: UpgradeMessage, + dismissTime: "never", + type: "success", + }); + }); + + void wb.register().then((reg) => { + registration = reg; + }); + }); +} diff --git a/FrontEnd/src/service-worker.txt b/FrontEnd/src/service-worker.txt deleted file mode 100644 index ea8dfc32..00000000 --- a/FrontEnd/src/service-worker.txt +++ /dev/null @@ -1,104 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "react-bootstrap"; - -import { pushAlert } from "./services/alert"; - -if ("serviceWorker" in navigator) { - let isThisTriggerUpgrade = false; - - const upgradeSuccessLocalStorageKey = "TIMELINE_UPGRADE_SUCCESS"; - - if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) { - pushAlert({ - message: "serviceWorker.upgradeSuccess", - type: "success", - }); - window.localStorage.removeItem(upgradeSuccessLocalStorageKey); - } - - void import("workbox-window").then(({ Workbox, messageSW }) => { - const wb = new Workbox("/sw.js"); - let registration: ServiceWorkerRegistration | undefined; - - // externalactivated is not usable but I still use its name. - wb.addEventListener("controlling", () => { - const upgradeReload = (): void => { - window.localStorage.setItem(upgradeSuccessLocalStorageKey, "true"); - window.location.reload(); - }; - - if (isThisTriggerUpgrade) { - upgradeReload(); - } else { - const Message: React.FC = () => { - const { t } = useTranslation(); - return ( - <> - {t("serviceWorker.externalActivatedPrompt")} - - - ); - }; - - pushAlert({ - message: Message, - dismissTime: "never", - type: "warning", - }); - } - }); - - wb.addEventListener("activated", (event) => { - if (!event.isUpdate) { - pushAlert({ - message: "serviceWorker.availableOffline", - type: "success", - }); - } - }); - - // Add an event listener to detect when the registered - // service worker has installed but is waiting to activate. - wb.addEventListener("waiting", (): void => { - const upgrade = (): void => { - isThisTriggerUpgrade = true; - if (registration && registration.waiting) { - // Send a message to the waiting service worker, - // instructing it to activate. - // Note: for this to work, you have to add a message - // listener in your service worker. See below. - void messageSW(registration.waiting, { type: "SKIP_WAITING" }); - } - }; - - const UpgradeMessage: React.FC = () => { - const { t } = useTranslation(); - return ( - <> - {t("serviceWorker.upgradePrompt")} - - - ); - }; - - pushAlert({ - message: UpgradeMessage, - dismissTime: "never", - type: "success", - }); - }); - - void wb.register().then((reg) => { - registration = reg; - }); - }); -} diff --git a/FrontEnd/src/sw.ts b/FrontEnd/src/sw.ts new file mode 100644 index 00000000..0130e345 --- /dev/null +++ b/FrontEnd/src/sw.ts @@ -0,0 +1,66 @@ +/// +/// +/// + +import { precacheAndRoute, matchPrecache } from "workbox-precaching"; +import { registerRoute, setDefaultHandler } from "workbox-routing"; +import { + NetworkFirst, + NetworkOnly, + StaleWhileRevalidate, +} from "workbox-strategies"; +import { CacheableResponsePlugin } from "workbox-cacheable-response"; +import { ExpirationPlugin } from "workbox-expiration"; + +declare let self: ServiceWorkerGlobalScope; + +self.addEventListener("message", (event) => { + if (event.data && (event.data as { type: string }).type === "SKIP_WAITING") { + void self.skipWaiting(); + } +}); + +precacheAndRoute(self.__WB_MANIFEST); + +const networkOnly = new NetworkOnly(); + +registerRoute(new RegExp("/swagger/?.*"), new NetworkOnly()); + +registerRoute(new RegExp("/api/token/?.*"), new NetworkOnly()); +registerRoute(new RegExp("/api/search/?.*"), new NetworkOnly()); + +registerRoute( + new RegExp("/api/users/.+/avatar"), + new StaleWhileRevalidate({ + cacheName: "avatars", + plugins: [ + new CacheableResponsePlugin({ + statuses: [200], + }), + new ExpirationPlugin({ + maxAgeSeconds: 60 * 60 * 24 * 30 * 3, // 3 months + }), + ], + }) +); + +registerRoute( + new RegExp("/api/?.*"), + new NetworkFirst({ + plugins: [ + new CacheableResponsePlugin({ + statuses: [200], + }), + ], + }) +); + +setDefaultHandler((options) => { + const { request } = options; + + if (request instanceof Request && request.destination === "document") + return matchPrecache("/index.html").then((r) => + r == null ? Response.error() : r + ); + else return networkOnly.handle(options); +}); diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx index 1b9d6d2a..df43d8d2 100644 --- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx +++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx @@ -1,6 +1,6 @@ import React from "react"; import classnames from "classnames"; -import { HubConnectionState } from "srcmicrosoft/signalr"; +import { HubConnectionState } from "@microsoft/signalr"; import { useTranslation } from "react-i18next"; export interface ConnectionStatusBadgeProps { diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json index 3afe2c3e..6b691e0e 100644 --- a/FrontEnd/tsconfig.json +++ b/FrontEnd/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": false, - "esModuleInterop": false, + "skipLibCheck": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, @@ -15,7 +13,6 @@ "jsx": "react", "noEmit": true, "types": ["vite/client"], - "sourceMap": true, "baseUrl": "./", "paths": { "@/*": ["src/*"] diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js index 2e85c36a..ee6f6931 100644 --- a/FrontEnd/vite.config.js +++ b/FrontEnd/vite.config.js @@ -3,10 +3,21 @@ */ import reactRefresh from "@vitejs/plugin-react-refresh"; +import { VitePWA } from "vite-plugin-pwa"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [reactRefresh()], + plugins: [ + reactRefresh(), + VitePWA({ + strategies: "injectManifest", + srcDir: "src", + filename: "sw.ts", + base: "/", + manifest: false, + includeAssets: "**", + }), + ], resolve: { alias: [{ find: "@", replacement: "/src" }], }, -- cgit v1.2.3 From b6afd5e8161126522bdfff876f5483fa97e94797 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 18:23:18 +0800 Subject: ... --- FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FrontEnd/src/views/timeline-common') diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx index 1f9f02a5..5f3f0345 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -138,7 +138,7 @@ const TimelinePostEdit: React.FC = (props) => { (kind === "text" && text.length !== 0) || (kind === "image" && image != null); - // eslint-disable-next-line srctypescript-eslint/no-non-null-assertion + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); const notifyHeightChange = (): void => { -- cgit v1.2.3