PageView.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import React, {
  2. useEffect, useMemo, useRef, useState,
  3. } from 'react';
  4. import type { IPagePopulatedToShowRevision } from '@growi/core';
  5. import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
  6. import { parseSlideFrontmatterInMarkdown } from '@growi/presentation';
  7. import dynamic from 'next/dynamic';
  8. import { useShouldExpandContent } from '~/client/services/layout';
  9. import type { RendererConfig } from '~/interfaces/services/renderer';
  10. import { generateSSRViewOptions } from '~/services/renderer/renderer';
  11. import {
  12. useIsEnabledMarp,
  13. useIsForbidden, useIsIdenticalPath, useIsNotCreatable,
  14. } from '~/stores/context';
  15. import { useSWRxCurrentPage, useIsNotFound } from '~/stores/page';
  16. import { useViewOptions } from '~/stores/renderer';
  17. import { useIsMobile } from '~/stores/ui';
  18. import type { CommentsProps } from '../Comments';
  19. import { PagePathNavSticky } from '../Common/PagePathNav';
  20. import { PageViewLayout } from '../Common/PageViewLayout';
  21. import { PageAlerts } from '../PageAlert/PageAlerts';
  22. import { PageContentFooter } from '../PageContentFooter';
  23. import type { PageSideContentsProps } from '../PageSideContents';
  24. import { SlideViewer } from '../ReactMarkdownComponents/SlideViewer';
  25. import { UserInfo } from '../User/UserInfo';
  26. import type { UsersHomepageFooterProps } from '../UsersHomepageFooter';
  27. import RevisionRenderer from './RevisionRenderer';
  28. import styles from './PageView.module.scss';
  29. const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
  30. const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
  31. const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
  32. const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
  33. const PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
  34. const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
  35. const UsersHomepageFooter = dynamic<UsersHomepageFooterProps>(() => import('../UsersHomepageFooter')
  36. .then(mod => mod.UsersHomepageFooter), { ssr: false });
  37. const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
  38. type Props = {
  39. pagePath: string,
  40. rendererConfig: RendererConfig,
  41. initialPage?: IPagePopulatedToShowRevision,
  42. }
  43. export const PageView = (props: Props): JSX.Element => {
  44. const commentsContainerRef = useRef<HTMLDivElement>(null);
  45. const [isCommentsLoaded, setCommentsLoaded] = useState(false);
  46. const {
  47. pagePath, initialPage, rendererConfig,
  48. } = props;
  49. const { data: isIdenticalPathPage } = useIsIdenticalPath();
  50. const { data: isForbidden } = useIsForbidden();
  51. const { data: isNotCreatable } = useIsNotCreatable();
  52. const { data: isNotFoundMeta } = useIsNotFound();
  53. const { data: isMobile } = useIsMobile();
  54. const { data: pageBySWR } = useSWRxCurrentPage();
  55. const { data: viewOptions } = useViewOptions();
  56. const page = pageBySWR ?? initialPage;
  57. const isNotFound = isNotFoundMeta || page == null;
  58. const isUsersHomepagePath = isUsersHomepage(pagePath);
  59. const shouldExpandContent = useShouldExpandContent(page);
  60. const { data: enabledMarp } = useIsEnabledMarp();
  61. // *************************** Auto Scroll ***************************
  62. useEffect(() => {
  63. // do nothing if hash is empty
  64. const { hash } = window.location;
  65. if (hash.length === 0) {
  66. return;
  67. }
  68. const targetId = hash.slice(1);
  69. const target = document.getElementById(decodeURIComponent(targetId));
  70. target?.scrollIntoView();
  71. }, [isCommentsLoaded]);
  72. // ******************************* end *******************************
  73. const specialContents = useMemo(() => {
  74. if (isIdenticalPathPage) {
  75. return <IdenticalPathPage />;
  76. }
  77. if (isForbidden) {
  78. return <ForbiddenPage />;
  79. }
  80. if (isNotCreatable) {
  81. return <NotCreatablePage />;
  82. }
  83. }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
  84. const headerContents = (
  85. <PagePathNavSticky pageId={page?._id} pagePath={pagePath} isWipPage={page?.wip} />
  86. );
  87. const sideContents = !isNotFound && !isNotCreatable
  88. ? (
  89. <PageSideContents page={page} />
  90. )
  91. : null;
  92. const footerContents = !isIdenticalPathPage && !isNotFound
  93. ? (
  94. <>
  95. {(isUsersHomepagePath && page.creator != null) && (
  96. <UsersHomepageFooter creatorId={page.creator._id} />
  97. )}
  98. <PageContentFooter page={page} />
  99. </>
  100. )
  101. : null;
  102. const Contents = () => {
  103. if (isNotFound || page?.revision == null) {
  104. return <NotFoundPage path={pagePath} />;
  105. }
  106. const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
  107. const markdown = page.revision.body;
  108. const [marp, useSlide] = viewOptions ? parseSlideFrontmatterInMarkdown(markdown) : [false, false];
  109. const useMarp = (enabledMarp ?? false) && marp;
  110. return (
  111. <>
  112. <PageContentsUtilities />
  113. <div className="flex-expand-vert justify-content-between">
  114. {
  115. (useMarp || useSlide)
  116. ? (<SlideViewer marp={useMarp}>{markdown}</SlideViewer>)
  117. : (<RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />)
  118. }
  119. { !isIdenticalPathPage && !isNotFound && (
  120. <div id="comments-container" ref={commentsContainerRef}>
  121. <Comments
  122. pageId={page._id}
  123. pagePath={pagePath}
  124. revision={page.revision}
  125. onLoaded={() => setCommentsLoaded(true)}
  126. />
  127. </div>
  128. ) }
  129. </div>
  130. </>
  131. );
  132. };
  133. const mobileClass = isMobile ? styles['page-mobile'] : '';
  134. return (
  135. <PageViewLayout
  136. headerContents={headerContents}
  137. sideContents={sideContents}
  138. footerContents={footerContents}
  139. expandContentWidth={shouldExpandContent}
  140. >
  141. <PageAlerts />
  142. {specialContents}
  143. {specialContents == null && (
  144. <>
  145. {(isUsersHomepagePath && page?.creator != null) && <UserInfo author={page.creator} />}
  146. <div className={`flex-expand-vert ${mobileClass}`}>
  147. <Contents />
  148. </div>
  149. </>
  150. )}
  151. </PageViewLayout>
  152. );
  153. };