aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx')
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx190
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;