diff options
Diffstat (limited to 'FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx')
-rw-r--r-- | FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx | 190 |
1 files changed, 190 insertions, 0 deletions
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<TimelinePageCardProps>; +} + +const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (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<HttpTimelineInfo | null>(null); + + const [connectionStatus, setConnectionStatus] = + React.useState<HubConnectionState>(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<number>(0); + + const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(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<boolean>(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 ? ( + <CardComponent + className="timeline-template-card" + timeline={timeline} + collapse={cardCollapse} + toggleCollapse={toggleCardCollapse} + onReload={onReload} + connectionStatus={connectionStatus} + /> + ) : null} + <Container + className="px-0" + style={{ + minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, + }} + > + {(() => { + if (state === "offline") { + // TODO: i18n + return <p className="text-danger">Offline!</p>; + } else if (state === "notexist") { + return <p className="text-danger">{t(props.notFoundI18nKey)}</p>; + } else if (state === "error") { + // TODO: i18n + return <p className="text-danger">Error!</p>; + } else { + return ( + <Timeline + timelineName={timeline?.name} + reloadKey={timelineReloadKey} + onReload={reloadTimeline} + onConnectionStateChanged={setConnectionStatus} + /> + ); + } + })()} + </Container> + {timeline != null && timeline.postable ? ( + <> + <div + style={{ height: bottomSpaceHeight }} + className="flex-fix-length" + /> + <TimelinePostEdit + className="fixed-bottom" + timeline={timeline} + onHeightChange={onPostEditHeightChange} + onPosted={reloadTimeline} + /> + </> + ) : null} + </> + ); +}; + +export default TimelinePageTemplate; |