aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx')
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx212
1 files changed, 212 insertions, 0 deletions
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
new file mode 100644
index 00000000..d74fffc4
--- /dev/null
+++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
@@ -0,0 +1,212 @@
+import React from 'react';
+import { Spinner } from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+import { Subject, fromEvent } from 'rxjs';
+
+import { getAlertHost } from '../common/alert-service';
+
+import Timeline, {
+ TimelinePostInfoEx,
+ TimelineDeleteCallback,
+} from './Timeline';
+import AppBar from '../common/AppBar';
+import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit';
+import CollapseButton from '../common/CollapseButton';
+
+export interface TimelineCardComponentProps<TTimeline, TManageItems> {
+ timeline: TTimeline;
+ onManage?: (item: TManageItems | 'property') => void;
+ onMember: () => void;
+ className?: string;
+ onHeight?: (height: number) => void;
+}
+
+export interface TimelinePageTemplateUIProps<
+ TTimeline extends { name: string },
+ TManageItems
+> {
+ avatarKey?: string | number;
+ timeline?: TTimeline;
+ posts?: TimelinePostInfoEx[] | 'forbid';
+ CardComponent: React.ComponentType<
+ TimelineCardComponentProps<TTimeline, TManageItems>
+ >;
+ onMember: () => void;
+ onManage?: (item: TManageItems | 'property') => void;
+ onPost?: TimelinePostSendCallback;
+ onDelete: TimelineDeleteCallback;
+ error?: string;
+}
+
+export default function TimelinePageTemplateUI<
+ TTimeline extends { name: string },
+ TEditItems
+>(
+ props: TimelinePageTemplateUIProps<TTimeline, TEditItems>
+): React.ReactElement | null {
+ const { timeline } = props;
+
+ const { t } = useTranslation();
+
+ const bottomSpaceRef = React.useRef<HTMLDivElement | null>(null);
+
+ const onPostEditHeightChange = React.useCallback((height: number): void => {
+ const { current: bottomSpaceDiv } = bottomSpaceRef;
+ if (bottomSpaceDiv != null) {
+ bottomSpaceDiv.style.height = `${height}px`;
+ }
+ 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 resizeSubject = React.useMemo(() => new Subject(), []);
+ const triggerResizeEvent = React.useCallback(() => {
+ resizeSubject.next(null);
+ }, [resizeSubject]);
+
+ React.useEffect(() => {
+ let scrollToBottom = true;
+ const disableScrollToBottom = (): void => {
+ scrollToBottom = false;
+ };
+
+ const subscriptions = [
+ fromEvent(window, 'wheel').subscribe(disableScrollToBottom),
+ fromEvent(window, 'pointerdown').subscribe(disableScrollToBottom),
+ fromEvent(window, 'keydown').subscribe(disableScrollToBottom),
+ resizeSubject.subscribe(() => {
+ if (scrollToBottom) {
+ window.scrollTo(0, document.body.scrollHeight);
+ }
+ }),
+ ];
+
+ return () => {
+ subscriptions.forEach((s) => s.unsubscribe());
+ };
+ }, [resizeSubject, timeline, props.posts]);
+
+ const [cardHeight, setCardHeight] = React.useState<number>(0);
+
+ const onCardHeightChange = React.useCallback((height: number) => {
+ setCardHeight(height);
+ }, []);
+
+ const genCardCollapseLocalStorageKey = (timelineName: string): string =>
+ `timeline.${timelineName}.cardCollapse`;
+
+ const cardCollapseLocalStorageKey =
+ timeline != null ? genCardCollapseLocalStorageKey(timeline.name) : null;
+
+ const [infoCardCollapse, setInfoCardCollapse] = React.useState<boolean>(true);
+ React.useEffect(() => {
+ if (cardCollapseLocalStorageKey != null) {
+ const savedCollapse =
+ window.localStorage.getItem(cardCollapseLocalStorageKey) === 'true';
+ setInfoCardCollapse(savedCollapse);
+ }
+ }, [cardCollapseLocalStorageKey]);
+
+ let body: React.ReactElement;
+
+ if (props.error != null) {
+ body = <p className="text-danger">{t(props.error)}</p>;
+ } else {
+ if (timeline != null) {
+ let timelineBody: React.ReactElement;
+ if (props.posts != null) {
+ if (props.posts === 'forbid') {
+ timelineBody = (
+ <p className="text-danger">{t('timeline.messageCantSee')}</p>
+ );
+ } else {
+ timelineBody = (
+ <Timeline
+ posts={props.posts}
+ onDelete={props.onDelete}
+ onResize={triggerResizeEvent}
+ />
+ );
+ if (props.onPost != null) {
+ timelineBody = (
+ <>
+ {timelineBody}
+ <div ref={bottomSpaceRef} className="flex-fix-length" />
+ <TimelinePostEdit
+ onPost={props.onPost}
+ onHeightChange={onPostEditHeightChange}
+ timelineName={timeline.name}
+ />
+ </>
+ );
+ }
+ }
+ } else {
+ timelineBody = (
+ <div className="full-viewport-center-child">
+ <Spinner color="primary" type="grow" />
+ </div>
+ );
+ }
+ const { CardComponent } = props;
+
+ body = (
+ <>
+ <div
+ className="fixed-top mt-appbar info-card-container"
+ data-collapse={infoCardCollapse ? 'true' : 'false'}
+ >
+ <CollapseButton
+ collapse={infoCardCollapse}
+ onClick={() => {
+ const newState = !infoCardCollapse;
+ setInfoCardCollapse(newState);
+ window.localStorage.setItem(
+ genCardCollapseLocalStorageKey(timeline.name),
+ newState.toString()
+ );
+ }}
+ className="float-right m-1 info-card-collapse-button text-orange"
+ />
+ <CardComponent
+ timeline={timeline}
+ onManage={props.onManage}
+ onMember={props.onMember}
+ onHeight={onCardHeightChange}
+ className="info-card-content"
+ />
+ </div>
+ {timelineBody}
+ </>
+ );
+ } else {
+ body = (
+ <div className="full-viewport-center-child">
+ <Spinner color="primary" type="grow" />
+ </div>
+ );
+ }
+ }
+
+ return (
+ <>
+ <AppBar />
+ <div>
+ <div
+ style={{ height: 56 + cardHeight }}
+ className="timeline-page-top-space flex-fix-length"
+ />
+ {body}
+ </div>
+ </>
+ );
+}