Leo 3 anni fa
parent
commit
53464a8a5f

+ 1 - 2
.eslintrc.json

@@ -1,7 +1,6 @@
 {
   "extends": ["next/core-web-vitals", "plugin:jsx-a11y/recommended"],
   "rules": {
-    "@next/next/no-img-element": "off",
-    "@typescript-eslint/no-unused-vars": "off"
+    "@next/next/no-img-element": "off"
   }
 }

+ 23 - 0
components/EmptyResult/index.module.scss

@@ -0,0 +1,23 @@
+.root {
+  @apply flex flex-col justify-center items-center text-neutral-400 py-20;
+  .icon {
+    @apply block w-20 h-20 mb-5;
+  }
+  h2 {
+    @apply text-2xl mb-3 text-neutral-900;
+  }
+  p {
+    @apply text-base;
+  }
+  a {
+    @apply text-blue-700 hover:underline;
+  }
+  :global(.dark) & {
+    h2 {
+      @apply text-neutral-50;
+    }
+    a {
+      @apply text-blue-400;
+    }
+  }
+}

+ 26 - 0
components/EmptyResult/index.tsx

@@ -0,0 +1,26 @@
+import styles from "./index.module.scss";
+
+interface EmptyResult {
+  className?: string;
+  icon?: string;
+  title?: string;
+  desc?: string;
+  children?: JSX.Element;
+}
+
+export default function EmptyResult(props: EmptyResult) {
+  const { icon, title, desc, children } = props;
+
+  return (
+    <div className={styles.root}>
+      {icon ? (
+        <svg className={styles.icon}>
+          <use xlinkHref={`/icons.svg#${icon}`}></use>
+        </svg>
+      ) : null}
+      {title ? <h2>{title}</h2> : null}
+      {desc ? <p>{desc}</p> : null}
+      {children}
+    </div>
+  );
+}

+ 7 - 7
components/library/LibrayList/Item.tsx

@@ -12,7 +12,8 @@ interface ItemProps {
 
 export default function Item({ type, data, onRemoved }: ItemProps) {
   const isHistory = type === "History";
-  const url = `/novel/${isHistory ? data.chapterUri : data.uri}`;
+  const url = `/novel/${data.uri}`;
+  const chapterUrl = `/novel/${data.chapterUri}`;
 
   const handleRemoveItem = () => {
     const item = { ...data };
@@ -21,10 +22,9 @@ export default function Item({ type, data, onRemoved }: ItemProps) {
     } else {
       item.isFavorite = 0;
     }
-
     (!item.isReading && !item.isFavorite
-      ? delHistoryItem(data.uri)
-      : putHistoryItem(data)
+      ? delHistoryItem(item.uri)
+      : putHistoryItem(item)
     ).then(onRemoved);
   };
 
@@ -42,7 +42,7 @@ export default function Item({ type, data, onRemoved }: ItemProps) {
           <Link href={url}>{data.name}</Link>
         </div>
         <div className={styles.progressTitle}>
-          <Link href={url}>
+          <Link href={chapterUrl}>
             You have read {data.chapterIndex}/{data.chapterCount}
           </Link>
         </div>
@@ -54,8 +54,8 @@ export default function Item({ type, data, onRemoved }: ItemProps) {
           ></div>
         </div>
         <div className={styles.btns}>
-          <Link className={styles.btn} href={url}>
-            Continue Reading
+          <Link className={styles.btn} href={chapterUrl}>
+            {data.isReading ? "Continue Reading" : "Start Reading"}
           </Link>
         </div>
       </div>

+ 27 - 10
components/library/LibrayList/index.tsx

@@ -1,5 +1,7 @@
+import EmptyResult from "components/EmptyResult";
 import Pagination from "components/Pagination";
 import useDbList from "libs/hooks/useDbList";
+import Link from "next/link";
 
 import styles from "./index.module.scss";
 import Item from "./Item";
@@ -35,16 +37,31 @@ export default function LibrayList({ type }: LibrayListProps) {
 
   return (
     <>
-      <ul className={styles.list}>
-        {historyList.map((item) => (
-          <Item
-            key={item.uri}
-            data={item}
-            type={type}
-            onRemoved={refreshList}
-          />
-        ))}
-      </ul>
+      {historyList.length ? (
+        <ul className={styles.list}>
+          {historyList.map((item) => (
+            <Item
+              key={item.uri}
+              data={item}
+              type={type}
+              onRemoved={refreshList}
+            />
+          ))}
+        </ul>
+      ) : (
+        <EmptyResult
+          icon={isHistory ? "browseGallery" : "autoStories"}
+          title={
+            isHistory
+              ? "You have no read history."
+              : "You haven't added any books yet."
+          }
+        >
+          <p>
+            <Link href="/">EXPLORE NOW</Link>
+          </p>
+        </EmptyResult>
+      )}
       {historyPages.total > historyPages.pageSize ? (
         <Pagination
           className="mt-5"

+ 2 - 0
package.json

@@ -16,6 +16,7 @@
     "moment": "^2.29.4",
     "next": "13.0.0",
     "next-pwa": "^5.6.0",
+    "nprogress": "^0.2.0",
     "qs": "^6.11.0",
     "react": "18.2.0",
     "react-dom": "18.2.0",
@@ -28,6 +29,7 @@
     "@tailwindcss/typography": "^0.5.7",
     "@types/gtag.js": "^0.0.12",
     "@types/node": "18.11.3",
+    "@types/nprogress": "^0.2.0",
     "@types/qs": "^6.9.7",
     "@types/react": "18.0.21",
     "@types/react-dom": "18.0.6",

+ 22 - 7
pages/_app.tsx

@@ -1,5 +1,6 @@
 import { SWRConfig } from "swr";
 import Script from "next/script";
+import NProgress from "nprogress";
 import type { NextPage } from "next";
 import { useRouter } from "next/router";
 import type { AppProps } from "next/app";
@@ -11,10 +12,11 @@ import { pageview } from "libs/gtag";
 import { Context } from "libs/context";
 import { GA_TRACKING_ID } from "libs/config";
 import Layout from "components/common/Layout";
+import { SeoHead, SeoHeadConfig } from "components/SeoHead";
 import getSiteConfig, { SiteConfig } from "libs/getSiteConfig";
 
+import "nprogress/nprogress.css";
 import "styles/globals.scss";
-import { SeoHead, SeoHeadConfig } from "components/SeoHead";
 
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
   getLayout?: (page: ReactElement) => ReactNode;
@@ -54,16 +56,29 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
   ]);
 
   useEffect(() => {
-    const handleRouteChange = (url: string) => {
+    const handleStart = () => {
+      NProgress.start();
+    };
+
+    const handleStop = () => {
+      NProgress.done();
+    };
+
+    const handleError = (url: string) => {
       pageview(url);
+      handleStop();
     };
-    router.events.on("routeChangeComplete", handleRouteChange);
-    router.events.on("hashChangeComplete", handleRouteChange);
+
+    router.events.on("routeChangeStart", handleStart);
+    router.events.on("routeChangeComplete", handleStop);
+    router.events.on("routeChangeError", handleError);
+
     return () => {
-      router.events.off("routeChangeComplete", handleRouteChange);
-      router.events.off("hashChangeComplete", handleRouteChange);
+      router.events.off("routeChangeStart", handleStart);
+      router.events.off("routeChangeComplete", handleStop);
+      router.events.off("routeChangeError", handleError);
     };
-  }, [router.events]);
+  }, [router]);
 
   return (
     <>

+ 14 - 11
pages/library.tsx

@@ -6,6 +6,8 @@ import TabPanel from "@mui/base/TabPanelUnstyled";
 
 import LibrayList from "components/library/LibrayList";
 
+import styles from "styles/library.module.scss";
+
 const tabs = ["History", "Favorite Books"];
 
 const Library = () => {
@@ -22,19 +24,20 @@ const Library = () => {
     <Tabs
       component={"main"}
       value={tab}
-      className="bg-paper py-3"
       onChange={handleTabChange}
     >
-      <div className="container">
-        <h1>{tab}</h1>
-        <TabsList className="tabs">
-          {tabs.map((item) => (
-            <Tab key={item} value={item} className="tab">
-              {item}
-            </Tab>
-          ))}
-        </TabsList>
-      </div>
+      <header className={styles["library-header"]}>
+        <div className="container">
+          <h1>{tab}</h1>
+          <TabsList className="tabs">
+            {tabs.map((item) => (
+              <Tab key={item} value={item} className="tab">
+                {item}
+              </Tab>
+            ))}
+          </TabsList>
+        </div>
+      </header>
       <div className="container">
         {tabs.map((item) => (
           <TabPanel key={item} value={item}>

+ 38 - 30
pages/novel/[slug]/index.tsx

@@ -1,5 +1,6 @@
-import Link from "next/link";
+import clsx from "clsx";
 import moment from "moment";
+import Link from "next/link";
 import { useMemo } from "react";
 import { useRouter } from "next/router";
 import Tab from "@mui/base/TabUnstyled";
@@ -37,9 +38,9 @@ function mergeHistory(
     img: detail.img,
     name: detail.name,
     status: detail.status,
-    chapterUri: history?.chapterUri ?? chapters[0].uri,
-    chapterName: history?.chapterName ?? chapters[0].name,
-    chapterCount: history?.chapterCount ?? chapters.length,
+    chapterUri: chapters[0].uri,
+    chapterName: chapters[0].name,
+    chapterCount: chapters.length,
     chapterIndex: history?.chapterIndex ?? 1,
     isReading: history?.isReading ?? 0,
     isFavorite: 1,
@@ -250,23 +251,8 @@ const Novel: NextPage<NovelPageProps> = (props) => {
               src={detail.img}
             />
             <div className={styles["nove-info-body"]}>
-              <h1>
-                {detail.name}
-                {/* <small>{detail.status}</small> */}
-              </h1>
+              <h1>{detail.name}</h1>
               <h2>
-                {/* {detail.genres && detail.genres.length > 0 ? (
-                  <Link
-                    title={detail.genres[0].name}
-                    href={`/novels/${detail.genres[0].uri}`}
-                  >
-                    <svg>
-                      <title>Genre: </title>
-                      <use xlinkHref="/icons.svg#paper"></use>
-                    </svg>
-                    <span>{detail.genres[0].name}</span>
-                  </Link>
-                ) : null} */}
                 <strong>
                   <svg>
                     <title>Chapters: </title>
@@ -297,20 +283,39 @@ const Novel: NextPage<NovelPageProps> = (props) => {
               </h2>
               <div className={styles["btns"]}>
                 <Link
-                  href={`/novel/${chapters.chapters[0].uri}`}
-                  className={styles["button"]}
+                  href={`/novel/${
+                    history?.isReading
+                      ? history?.chapterUri
+                      : chapters.chapters[0].uri
+                  }`}
+                  className={clsx(styles["button"], styles["button-reading"])}
                 >
-                  <strong>Start Reading</strong>
-                  <span>{chapters.chapters[0].name}</span>
+                  <strong>
+                    {history?.isReading ? "Continue Reading" : "Start Reading"}
+                  </strong>
+                  {history?.isReading ? (
+                    <small>{history.chapterName}</small>
+                  ) : null}
                 </Link>
                 <button
                   className={styles["button"]}
                   onClick={handleTogleFavorite}
                 >
                   <strong>
-                    {history && history.isFavorite
-                      ? "In Library"
-                      : "Add to Library"}
+                    <svg>
+                      <use
+                        xlinkHref={`/icons.svg#${
+                          history && history.isFavorite
+                            ? "favorite"
+                            : "unfavorite"
+                        }`}
+                      ></use>
+                    </svg>
+                    <span>
+                      {history && history.isFavorite
+                        ? "In Library"
+                        : "Add to Library"}
+                    </span>
                   </strong>
                 </button>
               </div>
@@ -318,8 +323,11 @@ const Novel: NextPage<NovelPageProps> = (props) => {
           </div>
         </div>
       </div>
-      <Tabs defaultValue={0} className="container bg-paper py-3">
-        <TabsList className="tabs">
+      <Tabs
+        defaultValue={0}
+        className={clsx(styles["novel-page"], "bg-paper", "py-3")}
+      >
+        <TabsList className={clsx("tabs", styles["novel-tabs"])}>
           <Tab value={0} className="tab">
             About
           </Tab>
@@ -351,7 +359,7 @@ const Novel: NextPage<NovelPageProps> = (props) => {
         <TabPanel value={1}>
           <h3 className="sub-title">{detail.name} Chapters</h3>
           <Tabs defaultValue={0}>
-            <TabsList className="tabs">
+            <TabsList className={clsx("tabs", styles["novel-tabs"], styles["chapter-tabs"])}>
               {chapterLists.map((chapter, idx) => (
                 <Tab value={idx} className="tab" key={chapter.title}>
                   {chapter.title}

+ 15 - 0
public/icons.svg

@@ -87,4 +87,19 @@
     <symbol id="last-page" viewBox="0 0 24 24">
         <path d="M5.59 7.41 10.18 12l-4.59 4.59L7 18l6-6-6-6zM16 6h2v12h-2z"></path>
     </symbol>
+
+    <symbol id="favorite" viewBox="0 0 24 24">
+        <path d="m12 21.35-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"></path>
+    </symbol>
+    <symbol id="unfavorite" viewBox="0 0 24 24">
+        <path d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z"></path>
+    </symbol>
+
+    <symbol id="autoStories" viewBox="0 0 24 24">
+        <path d="m19 1-5 5v11l5-4.5V1zM1 6v14.65c0 .25.25.5.5.5.1 0 .15-.05.25-.05C3.1 20.45 5.05 20 6.5 20c1.95 0 4.05.4 5.5 1.5V6c-1.45-1.1-3.55-1.5-5.5-1.5S2.45 4.9 1 6zm22 13.5V6c-.6-.45-1.25-.75-2-1v13.5c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5v2c1.35-.85 3.8-1.5 5.5-1.5 1.65 0 3.35.3 4.75 1.05.1.05.15.05.25.05.25 0 .5-.25.5-.5v-1.1z"></path>
+    </symbol>
+    <symbol id="browseGallery" viewBox="0 0 24 24">
+        <path d="M9 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm2.79 13.21L8 12.41V7h2v4.59l3.21 3.21-1.42 1.41z"></path>
+        <path d="M17.99 3.52v2.16C20.36 6.8 22 9.21 22 12c0 2.79-1.64 5.2-4.01 6.32v2.16C21.48 19.24 24 15.91 24 12s-2.52-7.24-6.01-8.48z"></path>
+    </symbol>
 </svg>

+ 60 - 29
styles/globals.scss

@@ -252,38 +252,10 @@
   @apply text-xs leading-none whitespace-nowrap text-ellipsis block overflow-hidden w-full;
 }
 .novel-cover {
-  @apply block before:block before:pt-[135%] relative;
+  @apply block before:block before:pt-[135%] relative select-none;
   &::after {
     @apply absolute left-0 top-0 h-full w-full;
     content: "";
-    // // background-image: repeating-linear-gradient(
-    // //   90deg,
-    // //   rgba(255, 255, 255, 0) 0%,
-    // //   rgba(255, 255, 255, 0.1) 10%,
-    // //   rgba(0, 0, 0, 0.5) 20%,
-    // //   rgba(0, 0, 0, 0.5) 50%,
-    // //   rgba(0, 0, 0, 0.1) 100%,
-    // // );
-    // background-image: linear-gradient(
-    //   90deg,
-    //   rgba(0, 0, 0, 0.2) 0%,
-    //   rgba(255, 255, 255, 0.3) 40%,
-    //   rgba(0, 0, 0, 0.5) 85%,
-    //   rgba(0, 0, 0, 0) 100%
-    // );
-    // background-size: 100% 100%;
-
-    // background-image: linear-gradient(
-    //     to right,
-    //     rgb(60, 13, 20) 0.7712082262210797%,
-    //     rgba(255, 255, 255, 0.5) 1.2853470437017995%,
-    //     rgba(255, 255, 255, 0.25) 1.7994858611825193%,
-    //     rgba(255, 255, 255, 0.25) 2.570694087403599%,
-    //     transparent 3.0848329048843186%,
-    //     transparent 4.113110539845758%,
-    //     rgba(255, 255, 255, 0.25) 4.370179948586118%,
-    //     transparent 5.655526992287918%
-    //   );
     background-image: linear-gradient(
         90deg,
         rgba(0, 0, 0, 0.4) 4px,
@@ -398,3 +370,62 @@
     @apply h-4 w-px bg-gray-500/20;
   }
 }
+#nprogress {
+  pointer-events: none;
+  .bar {
+    // background: ${color};
+    position: fixed;
+    z-index: 1031;
+    top: 0;
+    left: 0;
+    width: 100%;
+    // height: ${height}px;
+  }
+  .peg {
+    display: block;
+    position: absolute;
+    right: 0px;
+    width: 100px;
+    height: 100%;
+    // box-shadow: 0 0 10px ${color}, 0 0 5px ${color};
+    opacity: 1;
+    -webkit-transform: rotate(3deg) translate(0px, -4px);
+    -ms-transform: rotate(3deg) translate(0px, -4px);
+    transform: rotate(3deg) translate(0px, -4px);
+  }
+  .spinner {
+    display: "block";
+    position: fixed;
+    z-index: 1031;
+    top: 15px;
+    right: 15px;
+  }
+  .spinner-icon {
+    width: 18px;
+    height: 18px;
+    box-sizing: border-box;
+    border: solid 2px transparent;
+    // border-top-color: ${color};
+    // border-left-color: ${color};
+    border-radius: 50%;
+    -webkit-animation: nprogresss-spinner 400ms linear infinite;
+    animation: nprogress-spinner 400ms linear infinite;
+  }
+}
+.nprogress-custom-parent {
+  overflow: hidden;
+  position: relative;
+  #nprogress .spinner,
+  #nprogress .bar {
+    position: absolute;
+  }
+}
+
+@keyframes nprogress-spinner {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 16 - 0
styles/library.module.scss

@@ -0,0 +1,16 @@
+.library-header {
+  @apply bg-neutral-50 pt-6 border-b border-gray-900/10;
+
+  :global(.dark) {
+    @apply border-gray-50/[0.06];
+  }
+  h1 {
+    @apply text-4xl font-extrabold leading-loose mb-5;
+  }
+  :global(.tabs) {
+    @apply border-none;
+  }
+  :global(.tab) {
+    @apply text-xl;
+  }
+}

+ 27 - 5
styles/novel-info.module.scss

@@ -38,15 +38,25 @@
       }
     }
     .btns {
-      @apply flex flex-wrap gap-5;
+      @apply flex flex-wrap gap-5 z-30 fixed bottom-10 left-0 w-full px-5 lg:static lg:px-0;
     }
     .button {
-      @apply block text-white text-center px-4 py-3 rounded-md  lg:text-lg select-none bg-gradient-to-r from-blue-600 to-blue-800 transition-all;
+      @apply text-white text-center relative px-4 py-3 rounded-md lg:text-lg select-none bg-gradient-to-r from-blue-600 to-blue-800 transition-all flex flex-col justify-center items-center;
+      @apply hover:after:absolute hover:after:inset-0 hover:after:bg-white/5;
+      &.button-reading {
+        @apply w-0 flex-1 lg:w-auto lg:flex-none lg:max-w-md;
+      }
       strong {
-        @apply block uppercase;
+        @apply flex uppercase items-center justify-center text-center text-ellipsis overflow-hidden whitespace-nowrap;
+        svg {
+          @apply w-8 h-8 lg:mr-2;
+        }
+        span {
+          @apply hidden lg:block;
+        }
       }
-      span {
-        @apply block text-xs leading-3 opacity-70;
+      small {
+        @apply block text-xs leading-3 opacity-70 w-full text-ellipsis overflow-hidden whitespace-nowrap;
       }
       :global(.dark) & {
         @apply from-blue-600 to-blue-900;
@@ -93,3 +103,15 @@
     }
   }
 }
+.novel-page {
+  @apply container overflow-visible;
+}
+.novel-tabs {
+  @apply mb-5 sticky top-[57px] backdrop-blur bg-white/60 -mx-4 px-4 scroll-px-4 lg:top-[65px] lg:mx-0 lg:px-0 lg:scroll-px-0;
+  :global(.dark) & {
+    @apply bg-gray-900/75;
+  }
+  &.chapter-tabs {
+    @apply top-[106px] lg:top-[114px];
+  }
+}

+ 10 - 0
yarn.lock

@@ -1285,6 +1285,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.3.tgz#78a6d7ec962b596fc2d2ec102c4dd3ef073fea6a"
   integrity sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==
 
+"@types/nprogress@^0.2.0":
+  version "0.2.0"
+  resolved "https://registry.npmmirror.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
+  integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
+
 "@types/prop-types@*", "@types/prop-types@^15.7.5":
   version "15.7.5"
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
@@ -3141,6 +3146,11 @@ normalize-range@^0.1.2:
   resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
   integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
 
+nprogress@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
+  integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
+
 object-assign@^4.0.1, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"