PageView.tsx 6.7 KB

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