Leo 3 gadi atpakaļ
vecāks
revīzija
b51d6054cf

+ 98 - 0
components/VirtualScroll/index.tsx

@@ -0,0 +1,98 @@
+import clsx from "clsx";
+import {
+  ElementType,
+  UIEventHandler,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import styles from "./scroll.module.scss";
+
+interface VirtualScrollProps {
+  component?: ElementType;
+  className?: string | string[];
+  itemHeight: number;
+  total: number;
+  tolerance?: number;
+  startIndex?: number;
+  getItems: (offset: number, limit: number) => JSX.Element | JSX.Element[];
+}
+
+export default function VirtualScroll(props: VirtualScrollProps) {
+  const {
+    component: Component = "ul",
+    className,
+    itemHeight,
+    total,
+    startIndex = 0,
+    getItems,
+  } = props;
+
+  const [wrapHeight, setWrapHeight] = useState(0);
+
+  const [scrollY, setScrollY] = useState(startIndex * itemHeight);
+
+  const offset = useMemo(
+    () => Math.max(0, Math.floor(scrollY / itemHeight)),
+    [itemHeight, scrollY]
+  );
+
+  const limit = useMemo(() => {
+    return Math.floor(wrapHeight / itemHeight) + 1;
+  }, [itemHeight, wrapHeight]);
+
+  const [wrapElement, setWrapElement] = useState<HTMLDivElement | null>(null);
+
+  const handleScroll: UIEventHandler<HTMLDivElement> = (e) => {
+    setScrollY((e.target as HTMLDivElement).scrollTop);
+  };
+
+  const resize = useCallback(
+    (e: UIEvent) => {
+      if (wrapElement) setWrapHeight(wrapElement.clientHeight);
+    },
+    [wrapElement]
+  );
+
+  const setWrapRef = useCallback((ele: HTMLDivElement) => {
+    if (ele) {
+      setWrapElement(ele);
+      if (ele) setWrapHeight(ele.clientHeight);
+    } else {
+      setWrapElement(null);
+    }
+  }, []);
+
+  useEffect(() => {
+    if (window) {
+      window.addEventListener("resize", resize);
+    }
+
+    return () => {
+      window.removeEventListener("resize", resize);
+    };
+  }, [resize]);
+
+  return (
+    <div
+      ref={setWrapRef}
+      className={clsx(styles["wrap"], className)}
+      onScroll={handleScroll}
+    >
+      <div
+        className={styles["space"]}
+        style={{ height: `${total * itemHeight}px` }}
+      />
+      <Component
+        className={styles["inner"]}
+        style={{
+          transform: `translateY(${(scrollY % itemHeight) * -1}px)`,
+        }}
+      >
+        {getItems(offset, limit)}
+      </Component>
+    </div>
+  );
+}

+ 10 - 0
components/VirtualScroll/scroll.module.scss

@@ -0,0 +1,10 @@
+.wrap {
+  @apply flex items-start overflow-y-auto;
+
+  .space {
+    @apply w-0;
+  }
+  .inner {
+    @apply w-full self-start sticky top-0;
+  }
+}

+ 33 - 1
components/novel/Toc/index.tsx

@@ -3,6 +3,8 @@ import useGet from "../../../utils/hooks/useGet";
 import type { ChapterListData } from "../../../types/http";
 
 import styles from "../../../styles/chapter.module.scss";
+import { useCallback } from "react";
+import VirtualScroll from "../../VirtualScroll";
 
 type FontSize = 1 | 2 | 3 | 4 | 5;
 
@@ -18,10 +20,40 @@ const Toc = (props: TocProps) => {
     `/api/novel/${novel}/chapters`
   );
 
+  const getTocItem = useCallback(
+    (offset: number, limit: number) => {
+      if (!chapters) return [];
+
+      const list: JSX.Element[] = [];
+      const len = Math.min(limit + offset, chapters.chapters.length);
+      for (let i = offset; i < len; i++) {
+        const item = chapters.chapters[i];
+        list.push(
+          <li key={item.id}>
+            <Link href={`/novel/${item.uri}`} title={item.name} key={item.id}>
+              <i>1</i>
+              <strong>{item.name}</strong>
+            </Link>
+          </li>
+        );
+      }
+      return list;
+    },
+    [chapters]
+  );
+
   return (
     <div className={styles["toolbar-inner"]}>
       <div className={styles["toolbar-display-title"]}>Chapters</div>
       {chapters ? (
+        <VirtualScroll
+          component="ol"
+          itemHeight={32}
+          total={chapters.chapters.length}
+          getItems={getTocItem}
+        />
+      ) : null}
+      {/* {chapters ? (
         <ol>
           {chapters.chapters.map((item) => (
             <li key={item.id}>
@@ -32,7 +64,7 @@ const Toc = (props: TocProps) => {
             </li>
           ))}
         </ol>
-      ) : null}
+      ) : null} */}
     </div>
   );
 };

+ 8 - 3
components/novel/Toolbar/index.tsx

@@ -4,11 +4,12 @@ import styles from "../../../styles/chapter.module.scss";
 
 interface ToolbarProps {
   open: boolean;
+  type?: string;
   onChangeSettings: (type?: string) => void;
 }
 
 const Toolbar = (props: ToolbarProps) => {
-  const { open, onChangeSettings } = props;
+  const { open, type, onChangeSettings } = props;
 
   return (
     <div
@@ -18,7 +19,9 @@ const Toolbar = (props: ToolbarProps) => {
     >
       <div className={styles["toolbar-items"]}>
         <button
-          className={styles["toolbar-item"]}
+          className={clsx(styles["toolbar-item"], {
+            [styles["toolbar-item-current"]]: type === "toc",
+          })}
           title="Table Of Contents"
           onClick={() => onChangeSettings("toc")}
         >
@@ -27,7 +30,9 @@ const Toolbar = (props: ToolbarProps) => {
           </svg>
         </button>
         <button
-          className={styles["toolbar-item"]}
+          className={clsx(styles["toolbar-item"], {
+            [styles["toolbar-item-current"]]: type === "settings",
+          })}
           title="Display Options"
           onClick={() => onChangeSettings("settings")}
         >

+ 6 - 6
pages/novel/[slug]/[chapter].tsx

@@ -168,7 +168,11 @@ const Chapter: NextPageWithLayout = () => {
           ) : null}
         </div>
       </main>
-      <Toolbar open={open} onChangeSettings={handleChangeSettings} />
+      <Toolbar
+        open={open}
+        type={menu}
+        onChangeSettings={handleChangeSettings}
+      />
     </>
   );
 };
@@ -187,20 +191,16 @@ export const getServerSideProps: GetServerSideProps<
     };
   }
   const { slug, chapter } = context.params;
-  const [chapterData, chapters] = await Promise.all([
+  const [chapterData] = await Promise.all([
     get<ChapterData>(
       `https://novels.yergoo.com/api/novel/chapter/${slug}/${chapter}`
     ),
-    get<ChapterListData>(
-      `https://novels.yergoo.com/api/novel/${slug}/chapters`
-    ),
   ]);
 
   return {
     props: {
       fallback: {
         [`/api/novel/chapter/${slug}/${chapter}`]: chapterData,
-        [`/api/novel/${slug}/chapters`]: chapters,
       },
     },
   };

+ 7 - 4
styles/chapter.module.scss

@@ -60,6 +60,9 @@
     svg {
       @apply w-5 h-5 lg:w-4 lg:h-4;
     }
+    &.toolbar-item-current {
+      @apply lg:bg-blue-700 lg:text-white;
+    }
   }
 }
 .toolbar-display {
@@ -78,10 +81,10 @@
   // }
   .toolbar-inner {
     @apply max-h-[70vh] flex flex-col overflow-hidden flex-1 flex-shrink-0;
-    @apply lg:max-h-full px-5;
+    @apply lg:max-h-full lg:px-5;
     ol {
-      @apply flex-1 h-0 overflow-y-auto;
-      @apply lg:-mr-5 lg:pr-5;
+      // @apply flex-1 h-0 overflow-y-auto;
+      // @apply lg:-mr-5 lg:pr-5;
       li {
         a {
           @apply flex w-full h-full py-1;
@@ -95,7 +98,7 @@
           @apply w-9 mr-1 flex-shrink-0 not-italic text-slate-400;
         }
         strong {
-          @apply block font-normal flex-1;
+          @apply block font-normal flex-1 whitespace-nowrap w-0 overflow-hidden text-ellipsis;
         }
         small {
           @apply text-slate-400 ml-2;