PageView.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import React, {
  2. type JSX,
  3. useCallback,
  4. useEffect,
  5. useMemo,
  6. useRef,
  7. useState,
  8. } from 'react';
  9. import dynamic from 'next/dynamic';
  10. import type { IPagePopulatedToShowRevision } from '@growi/core';
  11. import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
  12. import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
  13. import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
  14. import type { RendererConfig } from '~/interfaces/services/renderer';
  15. import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
  16. import { generateSSRViewOptions } from '~/services/renderer/renderer';
  17. import { useIsNotFound, useSWRxCurrentPage } from '~/stores/page';
  18. import { useViewOptions } from '~/stores/renderer';
  19. import {
  20. useIsForbidden,
  21. useIsIdenticalPath,
  22. useIsNotCreatable,
  23. } from '~/stores-universal/context';
  24. import { UserInfo } from '../User/UserInfo';
  25. import { PageAlerts } from './PageAlerts/PageAlerts';
  26. import { PageContentFooter } from './PageContentFooter';
  27. import { PageViewLayout } from './PageViewLayout';
  28. import RevisionRenderer from './RevisionRenderer';
  29. const NotCreatablePage = dynamic(
  30. () =>
  31. import('~/client/components/NotCreatablePage').then(
  32. (mod) => mod.NotCreatablePage,
  33. ),
  34. { ssr: false },
  35. );
  36. const ForbiddenPage = dynamic(
  37. () => import('~/client/components/ForbiddenPage'),
  38. { ssr: false },
  39. );
  40. const NotFoundPage = dynamic(() => import('~/client/components/NotFoundPage'), {
  41. ssr: false,
  42. });
  43. const PageSideContents = dynamic(
  44. () =>
  45. import('~/client/components/PageSideContents').then(
  46. (mod) => mod.PageSideContents,
  47. ),
  48. { ssr: false },
  49. );
  50. const PageContentsUtilities = dynamic(
  51. () =>
  52. import('~/client/components/Page/PageContentsUtilities').then(
  53. (mod) => mod.PageContentsUtilities,
  54. ),
  55. { ssr: false },
  56. );
  57. const Comments = dynamic(
  58. () => import('~/client/components/Comments').then((mod) => mod.Comments),
  59. { ssr: false },
  60. );
  61. const UsersHomepageFooter = dynamic(
  62. () =>
  63. import('~/client/components/UsersHomepageFooter').then(
  64. (mod) => mod.UsersHomepageFooter,
  65. ),
  66. { ssr: false },
  67. );
  68. const IdenticalPathPage = dynamic(
  69. () =>
  70. import('~/client/components/IdenticalPathPage').then(
  71. (mod) => mod.IdenticalPathPage,
  72. ),
  73. { ssr: false },
  74. );
  75. const SlideRenderer = dynamic(
  76. () =>
  77. import('~/client/components/Page/SlideRenderer').then(
  78. (mod) => mod.SlideRenderer,
  79. ),
  80. { ssr: false },
  81. );
  82. type Props = {
  83. pagePath: string;
  84. rendererConfig: RendererConfig;
  85. initialPage?: IPagePopulatedToShowRevision;
  86. className?: string;
  87. };
  88. export const PageView = (props: Props): JSX.Element => {
  89. const commentsContainerRef = useRef<HTMLDivElement>(null);
  90. const { pagePath, initialPage, rendererConfig, className } = props;
  91. const { data: isIdenticalPathPage } = useIsIdenticalPath();
  92. const { data: isForbidden } = useIsForbidden();
  93. const { data: isNotCreatable } = useIsNotCreatable();
  94. const { data: isNotFoundMeta } = useIsNotFound();
  95. const { data: pageBySWR } = useSWRxCurrentPage();
  96. const { data: viewOptions } = useViewOptions();
  97. const page = pageBySWR ?? initialPage;
  98. const isNotFound = isNotFoundMeta || page == null;
  99. const isUsersHomepagePath = isUsersHomepage(pagePath);
  100. const shouldExpandContent = useShouldExpandContent(page);
  101. const markdown = page?.revision?.body;
  102. const isSlide = useSlidesByFrontmatter(
  103. markdown,
  104. rendererConfig.isEnabledMarp,
  105. );
  106. const [currentPageId, setCurrentPageId] = useState<string | undefined>(
  107. page?._id,
  108. );
  109. useEffect(() => {
  110. if (page?._id !== undefined) {
  111. setCurrentPageId(page._id);
  112. }
  113. }, [page?._id]);
  114. // *************************** Auto Scroll ***************************
  115. useEffect(() => {
  116. if (currentPageId == null) {
  117. return;
  118. }
  119. // do nothing if hash is empty
  120. const { hash } = window.location;
  121. if (hash.length === 0) {
  122. return;
  123. }
  124. const contentContainer = document.getElementById(
  125. 'page-view-content-container',
  126. );
  127. if (contentContainer == null) return;
  128. const targetId = decodeURIComponent(hash.slice(1));
  129. const target = document.getElementById(targetId);
  130. if (target != null) {
  131. target.scrollIntoView();
  132. return;
  133. }
  134. const observer = new MutationObserver(() => {
  135. const target = document.getElementById(targetId);
  136. if (target != null) {
  137. target.scrollIntoView();
  138. observer.disconnect();
  139. }
  140. });
  141. observer.observe(contentContainer, { childList: true, subtree: true });
  142. return () => observer.disconnect();
  143. }, [currentPageId]);
  144. // ******************************* end *******************************
  145. const specialContents = useMemo(() => {
  146. if (isIdenticalPathPage) {
  147. return <IdenticalPathPage />;
  148. }
  149. if (isForbidden) {
  150. return <ForbiddenPage />;
  151. }
  152. if (isNotCreatable) {
  153. return <NotCreatablePage />;
  154. }
  155. }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
  156. const headerContents = (
  157. <PagePathNavTitle
  158. pageId={page?._id}
  159. pagePath={pagePath}
  160. isWipPage={page?.wip}
  161. />
  162. );
  163. const sideContents =
  164. !isNotFound && !isNotCreatable ? <PageSideContents page={page} /> : null;
  165. const footerContents =
  166. !isIdenticalPathPage && !isNotFound ? (
  167. <>
  168. {isUsersHomepagePath && page.creator != null && (
  169. <UsersHomepageFooter creatorId={page.creator._id} />
  170. )}
  171. <PageContentFooter page={page} />
  172. </>
  173. ) : null;
  174. const Contents = useCallback(() => {
  175. if (isNotFound || page?.revision == null) {
  176. return <NotFoundPage path={pagePath} />;
  177. }
  178. const markdown = page.revision.body;
  179. const rendererOptions =
  180. viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
  181. return (
  182. <>
  183. <PageContentsUtilities />
  184. <div className="flex-expand-vert justify-content-between">
  185. {isSlide != null ? (
  186. <SlideRenderer marp={isSlide.marp} markdown={markdown} />
  187. ) : (
  188. <RevisionRenderer
  189. rendererOptions={rendererOptions}
  190. markdown={markdown}
  191. />
  192. )}
  193. {!isIdenticalPathPage && !isNotFound && (
  194. <div id="comments-container" ref={commentsContainerRef}>
  195. <Comments
  196. pageId={page._id}
  197. pagePath={pagePath}
  198. revision={page.revision}
  199. />
  200. </div>
  201. )}
  202. </div>
  203. </>
  204. );
  205. }, [
  206. isNotFound,
  207. page?.revision,
  208. page?._id,
  209. rendererConfig,
  210. pagePath,
  211. viewOptions,
  212. isSlide,
  213. isIdenticalPathPage,
  214. ]);
  215. return (
  216. <PageViewLayout
  217. className={className}
  218. headerContents={headerContents}
  219. sideContents={sideContents}
  220. footerContents={footerContents}
  221. expandContentWidth={shouldExpandContent}
  222. >
  223. <PageAlerts />
  224. {specialContents}
  225. {specialContents == null && (
  226. <>
  227. {isUsersHomepagePath && page?.creator != null && (
  228. <UserInfo author={page.creator} />
  229. )}
  230. <div id="page-view-content-container" className="flex-expand-vert">
  231. <Contents />
  232. </div>
  233. </>
  234. )}
  235. </PageViewLayout>
  236. );
  237. };