diff options
author | crupest <crupest@outlook.com> | 2020-09-03 21:10:58 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-09-03 21:10:58 +0800 |
commit | 6775b254270c8c7aaaee641181ad43e5558c0356 (patch) | |
tree | 9894ebd23a43ab9687f372cb3c18d15fe8a70caa /Timeline/ClientApp/src | |
parent | 4b529fc9f11b84161b9a1fc9e12b8116debce4e9 (diff) | |
download | timeline-6775b254270c8c7aaaee641181ad43e5558c0356.tar.gz timeline-6775b254270c8c7aaaee641181ad43e5558c0356.tar.bz2 timeline-6775b254270c8c7aaaee641181ad43e5558c0356.zip |
...
Diffstat (limited to 'Timeline/ClientApp/src')
9 files changed, 226 insertions, 194 deletions
diff --git a/Timeline/ClientApp/src/app/index.sass b/Timeline/ClientApp/src/app/index.sass index 42a89da5..3322e503 100644 --- a/Timeline/ClientApp/src/app/index.sass +++ b/Timeline/ClientApp/src/app/index.sass @@ -12,10 +12,6 @@ body margin: 0 -#app - display: flex - flex-direction: column - small line-height: 1.2 diff --git a/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx b/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx new file mode 100644 index 00000000..3c52150f --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import clsx from "clsx"; +import Svg from "react-inlinesvg"; +import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; +import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; + +const CollapseButton: React.FC<{ + collapse: boolean; + onClick: () => void; + className?: string; + style?: React.CSSProperties; +}> = ({ collapse, onClick, className, style }) => { + return ( + <Svg + src={collapse ? arrowsAngleExpandIcon : arrowsAngleContractIcon} + onClick={onClick} + className={clsx("text-primary icon-button", className)} + style={style} + /> + ); +}; + +export default CollapseButton; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx b/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx new file mode 100644 index 00000000..e67cfb43 --- /dev/null +++ b/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; + +import { UiLogicError } from "@/common"; + +export type TimelineSyncStatus = "syncing" | "synced" | "offline"; + +const SyncStatusBadge: React.FC<{ + status: TimelineSyncStatus; + style?: React.CSSProperties; + className?: string; +}> = ({ status, style, className }) => { + const { t } = useTranslation(); + + return ( + <div style={style} className={clsx("timeline-sync-state-badge", className)}> + {(() => { + switch (status) { + case "syncing": { + return ( + <> + <span className="timeline-sync-state-badge-pin bg-warning" /> + <span className="text-warning"> + {t("timeline.postSyncState.syncing")} + </span> + </> + ); + } + case "synced": { + return ( + <> + <span className="timeline-sync-state-badge-pin bg-success" /> + <span className="text-success"> + {t("timeline.postSyncState.synced")} + </span> + </> + ); + } + case "offline": { + return ( + <> + <span className="timeline-sync-state-badge-pin bg-danger" /> + <span className="text-danger"> + {t("timeline.postSyncState.offline")} + </span> + </> + ); + } + default: + throw new UiLogicError("Unknown sync state."); + } + })()} + </div> + ); +}; + +export default SyncStatusBadge; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx b/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx index 1ad62a51..1cb15d8e 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx @@ -51,10 +51,7 @@ const Timeline: React.FC<TimelineProps> = (props) => { }, [posts, onDelete]); return ( - <div - ref={props.containerRef} - className={clsx("container-fluid timeline", props.className)} - > + <div ref={props.containerRef} className={clsx("timeline", props.className)}> <div className="timeline-enter-animation-mask" /> {(() => { const length = posts.length; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx index ce371015..09d74d3c 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import Svg from "react-inlinesvg"; import chevronDownIcon from "bootstrap-icons/icons/chevron-down.svg"; import trashIcon from "bootstrap-icons/icons/trash.svg"; -import { Row, Col, Modal, Button } from "react-bootstrap"; +import { Modal, Button } from "react-bootstrap"; import { useAvatar } from "@/services/user"; import { TimelinePostInfo } from "@/services/timeline"; @@ -74,51 +74,45 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => { ); return ( - <Row + <div className={clsx( - "position-relative flex-nowrap", + "timeline-item position-relative", current && "current", props.className )} onClick={props.onClick} style={props.style} > - <Col className="timeline-line-area"> + <div className="timeline-line-area"> <div className="timeline-line-segment start"></div> <div className="timeline-line-node-container"> <div className="timeline-line-node"></div> </div> <div className="timeline-line-segment end"></div> {current && <div className="timeline-line-segment current-end" />} - </Col> - <Col className="timeline-pt-start"> - <Row className="flex-nowrap"> - <div className="col-auto flex-shrink-1 px-0"> - <Row className="ml-n3 mr-0 align-items-center"> - <span className="ml-3 text-primary white-space-no-wrap"> - {props.post.time.toLocaleString(i18n.languages)} - </span> - <small className="text-dark ml-3"> - {props.post.author.nickname} - </small> - </Row> - </div> + </div> + <div className="timeline-content-area"> + <div> + <span className="mr-2"> + <span className="text-primary white-space-no-wrap mr-2"> + {props.post.time.toLocaleString(i18n.languages)} + </span> + <small className="text-dark">{props.post.author.nickname}</small> + </span> {more != null ? ( - <div className="col-auto px-2 d-flex justify-content-center align-items-center"> - <Svg - src={chevronDownIcon} - className="text-info icon-button" - onClick={(e: Event) => { - more.toggle(); - e.stopPropagation(); - }} - /> - </div> + <Svg + src={chevronDownIcon} + className="text-info icon-button" + onClick={(e: Event) => { + more.toggle(); + e.stopPropagation(); + }} + /> ) : null} - </Row> - <div className="row d-block timeline-content"> + </div> + <div className="timeline-content"> <Link - className="float-right float-sm-left mx-2" + className="float-left m-2" to={"/users/" + props.post.author.username} > <BlobImage @@ -142,7 +136,7 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => { } })()} </div> - </Col> + </div> {more != null && more.isOpen ? ( <> <div @@ -169,7 +163,7 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => { ) : null} </> ) : null} - </Row> + </div> ); }; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index 4296a5ce..c2d4aeaa 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -1,11 +1,7 @@ -import React, { CSSProperties } from "react"; -import clsx from "clsx"; +import React from "react"; import { useTranslation } from "react-i18next"; import { fromEvent } from "rxjs"; -import Svg from "react-inlinesvg"; -import { Spinner, Collapse } from "react-bootstrap"; -import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; -import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; +import { Spinner } from "react-bootstrap"; import { getAlertHost } from "@/services/alert"; import { useEventEmiiter, UiLogicError } from "@/common"; @@ -21,63 +17,16 @@ import Timeline, { TimelineDeleteCallback, } from "./Timeline"; import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit"; - -type TimelinePostSyncState = "syncing" | "synced" | "offline"; - -const TimelinePostSyncStateBadge: React.FC<{ - state: TimelinePostSyncState; - style?: CSSProperties; - className?: string; -}> = ({ state, style, className }) => { - const { t } = useTranslation(); - - return ( - <div style={style} className={clsx("timeline-sync-state-badge", className)}> - {(() => { - switch (state) { - case "syncing": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-warning" /> - <span className="text-warning"> - {t("timeline.postSyncState.syncing")} - </span> - </> - ); - } - case "synced": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-success" /> - <span className="text-success"> - {t("timeline.postSyncState.synced")} - </span> - </> - ); - } - case "offline": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-danger" /> - <span className="text-danger"> - {t("timeline.postSyncState.offline")} - </span> - </> - ); - } - default: - throw new UiLogicError("Unknown sync state."); - } - })()} - </div> - ); -}; +import { TimelineSyncStatus } from "./SyncStatusBadge"; export interface TimelineCardComponentProps<TManageItems> { timeline: TimelineInfo; onManage?: (item: TManageItems | "property") => void; onMember: () => void; className?: string; + collapse: boolean; + syncStatus: TimelineSyncStatus; + toggleCollapse: () => void; } export interface TimelinePageTemplateUIProps<TManageItems> { @@ -216,22 +165,13 @@ export default function TimelinePageTemplateUI<TManageItems>( }) ); - const syncState: TimelinePostSyncState = postListState.syncing - ? "syncing" - : postListState.type === "synced" - ? "synced" - : "offline"; - timelineBody = ( - <div> - <TimelinePostSyncStateBadge state={syncState} /> - <Timeline - containerRef={timelineRef} - posts={posts} - onDelete={props.onDelete} - onResize={triggerResizeEvent} - /> - </div> + <Timeline + containerRef={timelineRef} + posts={posts} + onDelete={props.onDelete} + onResize={triggerResizeEvent} + /> ); if (props.onPost != null) { timelineBody = ( @@ -255,37 +195,35 @@ export default function TimelinePageTemplateUI<TManageItems>( </div> ); } + const { CardComponent } = props; + const syncStatus: TimelineSyncStatus = + postListState == null || postListState.syncing + ? "syncing" + : postListState.type === "synced" + ? "synced" + : "offline"; body = ( <> - <div className="info-card-container"> - <Svg - src={ - infoCardCollapse - ? arrowsAngleExpandIcon - : arrowsAngleContractIcon - } - onClick={() => { - const newState = !infoCardCollapse; - setInfoCardCollapse(newState); + <CardComponent + timeline={timeline} + onManage={props.onManage} + onMember={props.onMember} + className="timeline-info-card" + syncStatus={syncStatus} + collapse={infoCardCollapse} + toggleCollapse={() => { + const newState = !infoCardCollapse; + setInfoCardCollapse(newState); + if (timeline != null) { window.localStorage.setItem( genCardCollapseLocalStorageKey(timeline.uniqueId), newState.toString() ); - }} - className="float-right m-1 info-card-collapse-button text-primary icon-button" - /> - <Collapse in={!infoCardCollapse}> - <CardComponent - timeline={timeline} - onManage={props.onManage} - onMember={props.onMember} - className="info-card-content" - /> - </Collapse> - </div> - + } + }} + /> {timelineBody} </> ); diff --git a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass index a7b9af7b..ad024c78 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass +++ b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass @@ -1,11 +1,12 @@ @use 'sass:color' .timeline - display: flex - flex-direction: column z-index: 0 position: relative + &-item + display: flex + @keyframes timeline-enter-animation-mask-animation to height: 0 @@ -96,8 +97,9 @@ $timeline-line-color-current: #36c2e6 &-node animation-name: timeline-line-node-current -.timeline-pt-start +.timeline-content-area padding-top: 18px + flex-grow: 1 .timeline-item-delete-button position: absolute @@ -123,10 +125,6 @@ $timeline-line-color-current: #36c2e6 transition: height 0.5s .timeline-sync-state-badge - position: fixed - top: 0 - right: 0 - z-index: 1 font-size: 0.8em padding: 3px 8px border-radius: 5px @@ -140,7 +138,14 @@ $timeline-line-color-current: #36c2e6 vertical-align: middle margin-right: 0.6em -.info-card-container +.timeline-info-card position: sticky - top: 56px z-index: 1 + top: 56px + margin: 0.5em + + @include media-breakpoint-down(sm) + margin-bottom: 0 + + @include media-breakpoint-up(sm) + float: right diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx index 9f989148..764910aa 100644 --- a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx +++ b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx @@ -8,6 +8,8 @@ import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; import BlobImage from "../common/BlobImage"; import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; +import CollapseButton from "../timeline-common/CollapseButton"; +import SyncStatusBadge from "../timeline-common/SyncStatusBadge"; export type OrdinaryTimelineManageItem = "delete"; @@ -16,55 +18,75 @@ export type TimelineInfoCardProps = TimelineCardComponentProps< >; const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { - const { onMember, onManage } = props; + const { + timeline, + onMember, + onManage, + collapse, + syncStatus, + toggleCollapse, + } = props; const { t } = useTranslation(); - const avatar = useAvatar(props.timeline.owner.username); + const avatar = useAvatar(timeline?.owner?.username); return ( - <div className={clsx("rounded border p-2 bg-light", props.className)}> - <h3 className="text-primary mx-3 d-inline-block align-middle"> - {props.timeline.name} - </h3> - <div className="d-inline-block align-middle"> - <BlobImage blob={avatar} className="avatar small rounded-circle" /> - {props.timeline.owner.nickname} - <small className="ml-3 text-secondary"> - @{props.timeline.owner.username} - </small> + <div + className={clsx( + "rounded border p-2 bg-light", + props.className, + collapse && "align-self-end" + )} + > + <div className="float-right d-flex align-items-center"> + <SyncStatusBadge status={syncStatus} className="mr-2" /> + <CollapseButton collapse={collapse} onClick={toggleCollapse} /> </div> - <p className="mb-0">{props.timeline.description}</p> - <small className="mt-1 d-block"> - {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])} - </small> - <div className="text-right mt-2"> - {onManage != null ? ( - <Dropdown> - <Dropdown.Toggle variant="outline-primary"> - {t("timeline.manage")} - </Dropdown.Toggle> - <Dropdown.Menu> - <Dropdown.Item onClick={() => onManage("property")}> - {t("timeline.manageItem.property")} - </Dropdown.Item> - <Dropdown.Item onClick={onMember}> - {t("timeline.manageItem.member")} - </Dropdown.Item> - <Dropdown.Divider /> - <Dropdown.Item - className="text-danger" - onClick={() => onManage("delete")} - > - {t("timeline.manageItem.delete")} - </Dropdown.Item> - </Dropdown.Menu> - </Dropdown> - ) : ( - <Button variant="outline-primary" onClick={onMember}> - {t("timeline.memberButton")} - </Button> - )} + + <div style={{ display: collapse ? "none" : "block" }}> + <h3 className="text-primary mx-3 d-inline-block align-middle"> + {timeline.name} + </h3> + <div className="d-inline-block align-middle"> + <BlobImage blob={avatar} className="avatar small rounded-circle" /> + {timeline.owner.nickname} + <small className="ml-3 text-secondary"> + @{timeline.owner.username} + </small> + </div> + <p className="mb-0">{timeline.description}</p> + <small className="mt-1 d-block"> + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + </small> + <div className="text-right mt-2"> + {onManage != null ? ( + <Dropdown> + <Dropdown.Toggle variant="outline-primary"> + {t("timeline.manage")} + </Dropdown.Toggle> + <Dropdown.Menu> + <Dropdown.Item onClick={() => onManage("property")}> + {t("timeline.manageItem.property")} + </Dropdown.Item> + <Dropdown.Item onClick={onMember}> + {t("timeline.manageItem.member")} + </Dropdown.Item> + <Dropdown.Divider /> + <Dropdown.Item + className="text-danger" + onClick={() => onManage("delete")} + > + {t("timeline.manageItem.delete")} + </Dropdown.Item> + </Dropdown.Menu> + </Dropdown> + ) : ( + <Button variant="outline-primary" onClick={onMember}> + {t("timeline.memberButton")} + </Button> + )} + </div> </div> </div> ); diff --git a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx index cec81421..251e53b4 100644 --- a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx +++ b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx @@ -1,7 +1,6 @@ import React from "react"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; -import { fromEvent } from "rxjs"; import { Dropdown, Button } from "react-bootstrap"; import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; @@ -17,10 +16,10 @@ export type UserInfoCardProps = TimelineCardComponentProps< >; const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { - const { onManage } = props; + const { onManage, timeline } = props; const { t } = useTranslation(); - const avatar = useAvatar(props.timeline.owner.username); + const avatar = useAvatar(timeline?.owner?.username); return ( <div className={clsx("rounded border bg-light p-2", props.className)}> @@ -29,14 +28,14 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { className="avatar large mr-2 rounded-circle float-left" /> <div> - {props.timeline.owner.nickname} + {timeline.owner.nickname} <small className="ml-3 text-secondary"> - @{props.timeline.owner.username} + @{timeline.owner.username} </small> </div> - <p className="mb-0">{props.timeline.description}</p> + <p className="mb-0">{timeline.description}</p> <small className="mt-1 d-block"> - {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])} + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} </small> <div className="text-right mt-2"> {onManage != null ? ( |