PageView.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { type JSX, memo, useCallback, useId, 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 {
  10. useCurrentPageData,
  11. useCurrentPageId,
  12. useIsForbidden,
  13. useIsIdenticalPath,
  14. useIsNotCreatable,
  15. usePageNotFound,
  16. } from '~/states/page';
  17. import { useViewOptions } from '~/stores/renderer';
  18. import { UserInfo } from '../User/UserInfo';
  19. import { PageAlerts } from './PageAlerts/PageAlerts';
  20. import { PageContentFooter } from './PageContentFooter';
  21. import { PageViewLayout } from './PageViewLayout';
  22. import { useHashAutoScroll } from './use-hash-auto-scroll';
  23. // biome-ignore-start lint/style/noRestrictedImports: no-problem dynamic import
  24. const NotCreatablePage = dynamic(
  25. () =>
  26. import('~/client/components/NotCreatablePage').then(
  27. (mod) => mod.NotCreatablePage,
  28. ),
  29. { ssr: false },
  30. );
  31. const ForbiddenPage = dynamic(
  32. () => import('~/client/components/ForbiddenPage'),
  33. { ssr: false },
  34. );
  35. const NotFoundPage = dynamic(() => import('~/client/components/NotFoundPage'), {
  36. ssr: false,
  37. });
  38. const PageSideContents = dynamic(
  39. () =>
  40. import('~/client/components/PageSideContents').then(
  41. (mod) => mod.PageSideContents,
  42. ),
  43. { ssr: false },
  44. );
  45. const PageContentsUtilities = dynamic(
  46. () =>
  47. import('~/client/components/Page/PageContentsUtilities').then(
  48. (mod) => mod.PageContentsUtilities,
  49. ),
  50. { ssr: false },
  51. );
  52. const Comments = dynamic(
  53. () => import('~/client/components/Comments').then((mod) => mod.Comments),
  54. { ssr: false },
  55. );
  56. const UsersHomepageFooter = dynamic(
  57. () =>
  58. import('~/client/components/UsersHomepageFooter').then(
  59. (mod) => mod.UsersHomepageFooter,
  60. ),
  61. { ssr: false },
  62. );
  63. const IdenticalPathPage = dynamic(
  64. () =>
  65. import('~/client/components/IdenticalPathPage').then(
  66. (mod) => mod.IdenticalPathPage,
  67. ),
  68. { ssr: false },
  69. );
  70. const SlideRenderer = dynamic(
  71. () =>
  72. import('~/client/components/Page/SlideRenderer').then(
  73. (mod) => mod.SlideRenderer,
  74. ),
  75. { ssr: false },
  76. );
  77. const PageContentRenderer = dynamic(
  78. () => import('./PageContentRenderer').then((mod) => mod.PageContentRenderer),
  79. { ssr: true },
  80. );
  81. // biome-ignore-end lint/style/noRestrictedImports: no-problem dynamic import
  82. type Props = {
  83. pagePath: string;
  84. rendererConfig: RendererConfig;
  85. className?: string;
  86. };
  87. // Custom comparison function for memo to prevent unnecessary re-renders
  88. const arePropsEqual = (prevProps: Props, nextProps: Props): boolean =>
  89. prevProps.pagePath === nextProps.pagePath &&
  90. prevProps.className === nextProps.className &&
  91. isDeepEquals(prevProps.rendererConfig, nextProps.rendererConfig);
  92. const PageViewComponent = (props: Props): JSX.Element => {
  93. const commentsContainerRef = useRef<HTMLDivElement>(null);
  94. const { pagePath, rendererConfig, className } = props;
  95. const currentPageId = useCurrentPageId();
  96. const isIdenticalPathPage = useIsIdenticalPath();
  97. const isForbidden = useIsForbidden();
  98. const isNotCreatable = useIsNotCreatable();
  99. const isNotFoundMeta = usePageNotFound();
  100. const contentContainerId = useId();
  101. const page = useCurrentPageData();
  102. const { data: viewOptions } = useViewOptions();
  103. const isNotFound = isNotFoundMeta || page == null;
  104. const isUsersHomepagePath = isUsersHomepage(pagePath);
  105. const shouldExpandContent = useShouldExpandContent(page);
  106. const markdown = page?.revision?.body;
  107. const isSlide = useSlidesByFrontmatter(
  108. markdown,
  109. rendererConfig.isEnabledMarp,
  110. );
  111. // Auto-scroll to URL hash target, handling lazy-rendered content
  112. useHashAutoScroll({ key: currentPageId, contentContainerId });
  113. const specialContents = useMemo(() => {
  114. if (isIdenticalPathPage) {
  115. return <IdenticalPathPage />;
  116. }
  117. if (isForbidden) {
  118. return <ForbiddenPage />;
  119. }
  120. if (isNotCreatable) {
  121. return <NotCreatablePage />;
  122. }
  123. }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
  124. const headerContents = (
  125. <PagePathNavTitle
  126. pageId={page?._id}
  127. pagePath={pagePath}
  128. isWipPage={page?.wip}
  129. />
  130. );
  131. const sideContents =
  132. !isNotFound && !isNotCreatable ? <PageSideContents page={page} /> : null;
  133. const footerContents =
  134. !isIdenticalPathPage && !isNotFound ? (
  135. <>
  136. {isUsersHomepagePath && page.creator != null && (
  137. <UsersHomepageFooter creatorId={page.creator._id} />
  138. )}
  139. <PageContentFooter page={page} />
  140. </>
  141. ) : null;
  142. const Contents = useCallback(() => {
  143. if (isNotFound || page?.revision == null) {
  144. return <NotFoundPage path={pagePath} />;
  145. }
  146. const markdown = page.revision.body;
  147. return (
  148. <>
  149. <PageContentsUtilities />
  150. <div className="flex-expand-vert justify-content-between">
  151. {isSlide != null ? (
  152. <SlideRenderer marp={isSlide.marp} markdown={markdown} />
  153. ) : (
  154. <PageContentRenderer
  155. rendererOptions={viewOptions}
  156. rendererConfig={rendererConfig}
  157. pagePath={pagePath}
  158. markdown={markdown}
  159. />
  160. )}
  161. {!isIdenticalPathPage && !isNotFound && (
  162. <div id="comments-container" ref={commentsContainerRef}>
  163. <Comments
  164. pageId={page._id}
  165. pagePath={pagePath}
  166. revision={page.revision}
  167. />
  168. </div>
  169. )}
  170. </div>
  171. </>
  172. );
  173. }, [
  174. isNotFound,
  175. page?.revision,
  176. page?._id,
  177. rendererConfig,
  178. pagePath,
  179. viewOptions,
  180. isSlide,
  181. isIdenticalPathPage,
  182. page,
  183. ]);
  184. return (
  185. <PageViewLayout
  186. className={className}
  187. headerContents={headerContents}
  188. sideContents={sideContents}
  189. footerContents={footerContents}
  190. expandContentWidth={shouldExpandContent}
  191. >
  192. <PageAlerts />
  193. {specialContents}
  194. {specialContents == null && (
  195. <>
  196. {isUsersHomepagePath && page?.creator != null && (
  197. <UserInfo author={page.creator} />
  198. )}
  199. <div id={contentContainerId} className="flex-expand-vert">
  200. <Contents />
  201. </div>
  202. </>
  203. )}
  204. </PageViewLayout>
  205. );
  206. };
  207. export const PageView = memo(PageViewComponent, arePropsEqual);
  208. PageView.displayName = 'PageView';