Lsx.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import React, { useCallback, useMemo } from 'react';
  2. import { useSWRxLsx } from '../stores/lsx';
  3. import { generatePageNodeTree } from '../utils/page-node';
  4. import { LsxListView } from './LsxPageList/LsxListView';
  5. import { LsxContext } from './lsx-context';
  6. import styles from './Lsx.module.scss';
  7. type Props = {
  8. children: React.ReactNode,
  9. className?: string,
  10. prefix: string,
  11. num?: string,
  12. depth?: string,
  13. sort?: string,
  14. reverse?: string,
  15. filter?: string,
  16. except?: string,
  17. isImmutable?: boolean,
  18. isSharedPage?: boolean,
  19. };
  20. const LsxSubstance = React.memo(({
  21. prefix,
  22. num, depth, sort, reverse, filter, except,
  23. isImmutable,
  24. }: Props): JSX.Element => {
  25. const lsxContext = useMemo(() => {
  26. const options = {
  27. num, depth, sort, reverse, filter, except,
  28. };
  29. return new LsxContext(prefix, options);
  30. }, [depth, filter, num, prefix, reverse, sort, except]);
  31. const {
  32. data, error, isLoading, setSize,
  33. } = useSWRxLsx(lsxContext.pagePath, lsxContext.options, isImmutable);
  34. const hasError = error != null;
  35. const errorMessage = error?.message;
  36. const Error = useCallback((): JSX.Element => {
  37. if (!hasError) {
  38. return <></>;
  39. }
  40. return (
  41. <details>
  42. <summary className="text-warning">
  43. <i className="fa fa-exclamation-triangle fa-fw"></i> {lsxContext.toString()}
  44. </summary>
  45. <small className="ml-3 text-muted">{errorMessage}</small>
  46. </details>
  47. );
  48. }, [errorMessage, hasError, lsxContext]);
  49. const Loading = useCallback((): JSX.Element => {
  50. if (hasError) {
  51. return <></>;
  52. }
  53. if (!isLoading) {
  54. return <></>;
  55. }
  56. return (
  57. <div className={`text-muted ${isLoading ? 'lsx-blink' : ''}`}>
  58. <small>
  59. <i className="fa fa-spinner fa-pulse mr-1"></i>
  60. {lsxContext.toString()}
  61. </small>
  62. </div>
  63. );
  64. }, [hasError, isLoading, lsxContext]);
  65. const contents = useMemo(() => {
  66. if (data == null) {
  67. return <></>;
  68. }
  69. const depthRange = lsxContext.getOptDepth();
  70. const nodeTree = generatePageNodeTree(prefix, data.flatMap(d => d.pages), depthRange);
  71. const basisViewersCount = data.at(-1)?.toppageViewersCount;
  72. return <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={basisViewersCount} />;
  73. }, [data, lsxContext, prefix]);
  74. const LoadMore = useCallback(() => {
  75. const lastResult = data?.at(-1);
  76. if (lastResult == null) {
  77. return <></>;
  78. }
  79. const { cursor, total } = lastResult;
  80. const leftItemsNum = total - cursor;
  81. if (leftItemsNum === 0) {
  82. return <></>;
  83. }
  84. return (
  85. <div className="row justify-content-center lsx-load-more-row">
  86. <div className="col-12 col-sm-8 d-flex flex-column align-items-center lsx-load-more-container">
  87. <button
  88. type="button"
  89. className="btn btn btn-block btn-outline-secondary btn-load-more"
  90. onClick={() => setSize(size => size + 1)}
  91. >
  92. Load more<br />
  93. <span className="text-muted small left-items-label">({leftItemsNum} pages left)</span>
  94. </button>
  95. </div>
  96. </div>
  97. );
  98. }, [data, setSize]);
  99. return (
  100. <div className={`lsx ${styles.lsx}`}>
  101. <Error />
  102. <Loading />
  103. {contents}
  104. <LoadMore />
  105. </div>
  106. );
  107. });
  108. LsxSubstance.displayName = 'LsxSubstance';
  109. const LsxDisabled = React.memo((): JSX.Element => {
  110. return (
  111. <div className="text-muted">
  112. <i className="fa fa-fw fa-info-circle"></i>
  113. <small>lsx is not available on the share link page</small>
  114. </div>
  115. );
  116. });
  117. LsxDisabled.displayName = 'LsxDisabled';
  118. export const Lsx = React.memo((props: Props): JSX.Element => {
  119. if (props.isSharedPage) {
  120. return <LsxDisabled />;
  121. }
  122. return <LsxSubstance {...props} />;
  123. });
  124. Lsx.displayName = 'Lsx';
  125. export const LsxImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
  126. return <Lsx {...props} isImmutable />;
  127. });
  128. LsxImmutable.displayName = 'LsxImmutable';