PageView.tsx 7.4 KB

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