SilenceLeo %!s(int64=3) %!d(string=hai) anos
pai
achega
1d575d8d80

+ 15 - 8
components/common/Header/index.tsx

@@ -1,14 +1,19 @@
 import clsx from "clsx";
 import Link from "next/link";
-import { useCallback, useState } from "react";
+import { useRouter } from "next/router";
+import { useCallback, useMemo, useState } from "react";
 import ClickAwayListener from "@mui/base/ClickAwayListener";
 
-import toggleTheme from "libs/toggleTheme";
 import useStore from "libs/hooks/useStore";
+import toggleTheme from "libs/toggleTheme";
 
 export default function Header() {
   const store = useStore();
   const [open, setOpen] = useState(false);
+  const { pathname } = useRouter();
+  const needSearch = useMemo(() => {
+    return !/\/novels/.test(pathname);
+  }, [pathname]);
 
   const toggleMenu = useCallback(() => {
     setOpen((o) => !o);
@@ -74,12 +79,14 @@ export default function Header() {
                 </li>
               </ul>
             </nav>
-            {/* <form action="./search" className="search-input">
-              <svg className="icon-search">
-                <use xlinkHref="/icons.svg#search"></use>
-              </svg>
-              <input type="search" name="q" placeholder="Search" />
-            </form> */}
+            {needSearch ? (
+              <form action="/novels" className="search-input">
+                <svg className="icon-search">
+                  <use xlinkHref="/icons.svg#search"></use>
+                </svg>
+                <input type="search" name="q" placeholder="Search" />
+              </form>
+            ) : null}
           </div>
           <div className="buttons">
             <button className="btn" onClick={() => toggleTheme()}>

+ 43 - 0
components/common/SearchForm/index.tsx

@@ -0,0 +1,43 @@
+import { useRouter } from "next/router";
+import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react";
+
+export default function SearchForm() {
+  const { asPath, query, push } = useRouter();
+  const inSearchPage = useMemo(() => {
+    return !/\/novels/.test(asPath);
+  }, [asPath]);
+
+  const [value, setValue] = useState("");
+
+  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    push(`${asPath.split("?")[0]}${value ? `?q=${value}` : ""}`);
+  };
+
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
+    setValue(e.target.value);
+  };
+
+  useEffect(() => {
+    setValue((query.q as string) || "");
+  }, [query.q]);
+
+  return (
+    <form
+      action={inSearchPage ? asPath : "/novels"}
+      className="search-input"
+      onSubmit={handleSubmit}
+    >
+      <svg className="icon-search">
+        <use xlinkHref="/icons.svg#search"></use>
+      </svg>
+      <input
+        type="search"
+        name="q"
+        placeholder="Search"
+        value={value}
+        onChange={handleChange}
+      />
+    </form>
+  );
+}

+ 2 - 1
libs/db/getDb.ts

@@ -16,7 +16,8 @@ export interface NovelDb extends DBSchema {
 export default function getDb() {
   return openDB<NovelDb>("NovelDit", 3, {
     upgrade(db, oldVersion, newVersion) {
-      if (newVersion && newVersion > oldVersion) {
+      console.log(db, oldVersion, newVersion);
+      if (oldVersion && newVersion && newVersion > oldVersion) {
         db.deleteObjectStore("history");
       }
 

+ 6 - 0
pages/_app.tsx

@@ -125,6 +125,12 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
         <meta name="msapplication-TileColor" content="#5b5b5b" />
         <meta name="msapplication-TileImage" content={siteConfig.touchIcon} />
         <meta name="msapplication-tooltip" content={siteConfig.title} />
+        <link
+          rel="search"
+          type="application/opensearchdescription+xml"
+          href="/opensearch.xml"
+          title={siteConfig.siteName}
+        />
       </SeoHead>
       <Script
         strategy="afterInteractive"

+ 48 - 12
pages/index.tsx

@@ -3,21 +3,57 @@ import useGet from "libs/hooks/useGet";
 import NovelItem from "components/NovelItem";
 
 const Home = () => {
-  const { data } = useGet<ListItem[]>("/api/list");
+  const { data } = useGet<HomeData>("/api/list");
 
+  if (!data) return null;
+  const { completed, on_going, rands } = data.data;
   return (
     <main className="container">
-      <h2 className="novel-title">Popular This Week</h2>
-      <ul className="novel-list">
-        {(data?.data || []).map((item) => (
-          <NovelItem
-            key={item.uri}
-            slug={item.uri}
-            img={item.img}
-            name={item.name}
-          />
-        ))}
-      </ul>
+      {rands ? (
+        <>
+          <h2 className="novel-title">Ranking</h2>
+          <ul className="novel-list">
+            {rands.map((item) => (
+              <NovelItem
+                key={item.uri}
+                slug={item.uri}
+                img={item.img}
+                name={item.name}
+              />
+            ))}
+          </ul>
+        </>
+      ) : null}
+      {on_going ? (
+        <>
+          <h2 className="novel-title">Updating</h2>
+          <ul className="novel-list">
+            {on_going.map((item) => (
+              <NovelItem
+                key={item.uri}
+                slug={item.uri}
+                img={item.img}
+                name={item.name}
+              />
+            ))}
+          </ul>
+        </>
+      ) : null}
+      {completed ? (
+        <>
+          <h2 className="novel-title">Completed</h2>
+          <ul className="novel-list">
+            {completed.map((item) => (
+              <NovelItem
+                key={item.uri}
+                slug={item.uri}
+                img={item.img}
+                name={item.name}
+              />
+            ))}
+          </ul>
+        </>
+      ) : null}
     </main>
   );
 };

+ 102 - 27
pages/novels/[genre].tsx

@@ -1,6 +1,6 @@
 import clsx from "clsx";
 import Link from "next/link";
-import { useContext, useMemo } from "react";
+import { FormEvent, useContext, useMemo } from "react";
 import { useRouter } from "next/router";
 import { GetServerSideProps } from "next";
 
@@ -12,18 +12,42 @@ import NovelItem from "components/NovelItem";
 import styles from "styles/genre.module.scss";
 import useStore from "libs/hooks/useStore";
 import { SeoHead, SeoHeadConfig } from "components/SeoHead";
+import Pagination from "components/Pagination";
+import PaginationItem from "components/Pagination/PaginationItem";
+import SearchForm from "components/common/SearchForm";
+import EmptyResult from "components/EmptyResult";
+
+interface Query {
+  genre?: string;
+  page?: string;
+  q?: string;
+}
+
+function getKey(query: Query = {}) {
+  const queryString = `?page=${
+    isNaN(Number(query.page)) ? "1" : query.page
+  }&size=20`;
+
+  return query.q
+    ? `/api/search${queryString}${
+        query.genre ? `&genre=${query.genre}` : ""
+      }&name=${query.q}`
+    : query.genre
+    ? `/api/genre/${query.genre}${queryString}`
+    : `/api/all${queryString}`;
+}
 
 const Genre = () => {
-  const { query } = useRouter();
-  const { siteConfig } = useStore();
-  const { data } = useGet<ListItem[]>(
-    query.genre ? `/api/genre/${query.genre}` : "/api/list"
-  );
-  const store = useContext(Context);
+  const { query, asPath } = useRouter();
+  const { genre, siteConfig } = useStore();
+  const { data } = useGet<NovelList>(getKey(query));
   const currentName = useMemo(() => {
-    const item = store.genre.find((item) => item.uri === query.genre);
+    const item = genre.find((item) => item.uri === query.genre);
     return item ? item.name : "All";
-  }, [query.genre, store.genre]);
+  }, [query.genre, genre]);
+  const pashName = useMemo(() => {
+    return asPath.split("?")[0];
+  }, [asPath]);
 
   const seoConfig: SeoHeadConfig = useMemo(() => {
     // const genreName =
@@ -40,6 +64,31 @@ const Genre = () => {
       url: `https://${siteConfig.host}/novels${
         query.genre ? `/${query.genre}` : ""
       }`,
+      canonical: `https://${siteConfig.host}/novels${
+        query.genre ? `/${query.genre}` : ""
+      }`,
+      ...(data?.data.pageIndex && data.data.pageIndex > 1
+        ? {
+            pre: `https://${siteConfig.host}/novels${
+              query.genre ? `/${query.genre}` : ""
+            }${query.q ? `?q=${query.q}` : ""}${
+              data.data.pageIndex - 1 === 1
+                ? ""
+                : `${query.q ? `&` : "?"}page=${data.data.pageIndex - 1}`
+            }`,
+          }
+        : {}),
+      ...(data?.data.pageSize &&
+      data?.data.pageIndex &&
+      data.data.pageSize > data.data.pageIndex
+        ? {
+            next: `https://${siteConfig.host}/novels${
+              query.genre ? `/${query.genre}` : ""
+            }${query.q ? `?q=${query.q}` : ""}${`${query.q ? `&` : "?"}page=${
+              data.data.pageIndex + 1
+            }`}`,
+          }
+        : {}),
       jsonLd: JSON.stringify([
         {
           "@context": "https://schema.org",
@@ -72,7 +121,7 @@ const Genre = () => {
         {
           "@context": "https://schema.org",
           "@type": "ItemList",
-          itemListElement: (data?.data || []).map((item, idx) => ({
+          itemListElement: (data?.data.rows || []).map((item, idx) => ({
             "@type": "ListItem",
             position: idx + 1,
             url: `https://${siteConfig.host}/novel/${item.uri}`,
@@ -88,7 +137,7 @@ const Genre = () => {
     };
   }, [
     currentName,
-    data?.data,
+    data?.data.rows,
     query.genre,
     siteConfig.host,
     siteConfig.jsonLd,
@@ -110,7 +159,7 @@ const Genre = () => {
         >
           All
         </Link>
-        {store.genre.map((item) => (
+        {genre.map((item) => (
           <Link
             href={`/novels/${item.uri}`}
             key={item.uri}
@@ -123,26 +172,52 @@ const Genre = () => {
           </Link>
         ))}
       </div>
+      <h3 className="novel-title">Search</h3>
+      <SearchForm />
       <h2 className="novel-title">{`${currentName} Novels`}</h2>
-      <ul className="novel-list">
-        {(data?.data || []).map((item) => (
-          <NovelItem
-            key={item.uri}
-            slug={item.uri}
-            img={item.img}
-            name={item.name}
-          />
-        ))}
-      </ul>
+      {data?.data.rows && data.data.rows.length > 0 ? (
+        <ul className="novel-list">
+          {(data?.data.rows || []).map((item) => (
+            <NovelItem
+              key={item.uri}
+              slug={item.uri}
+              img={item.img}
+              name={item.name}
+            />
+          ))}
+        </ul>
+      ) : (
+        <EmptyResult
+          icon="autoStories"
+          title={`Couldn't find books${
+            query.q ? ` matching "${query.q}"` : ""
+          }.`}
+        />
+      )}
+      {data?.data.total_pages && data.data.total_pages > 1 ? (
+        <Pagination
+          className="mt-5"
+          count={data?.data.total_pages}
+          page={data?.data.pageIndex}
+          renderItem={(item) => (
+            <PaginationItem
+              component={Link}
+              href={`${pashName}${query.q ? `?q=${query.q}` : ""}${
+                item.page === 1 ? "" : `${query.q ? `&` : "?"}page=${item.page}`
+              }`}
+              {...item}
+            />
+          )}
+        />
+      ) : null}
     </main>
   );
 };
 
-export const getServerSideProps: GetServerSideProps<
-  { fallback: Docs },
-  { genre: string }
-> = async ({ params }) => {
-  const key = params?.genre ? `/api/genre/${params.genre}` : `/api/list`;
+export const getServerSideProps: GetServerSideProps<{
+  fallback: Docs;
+}> = async ({ query }) => {
+  const key = getKey(query);
   const data = await get(key);
 
   return {

+ 24 - 0
pages/opensearch.xml.tsx

@@ -0,0 +1,24 @@
+import { GetServerSideProps } from "next";
+import getSiteConfig from "libs/getSiteConfig";
+
+export default function Opensearch(host: ReturnType<typeof getSiteConfig>) {
+  return `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+                       xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+  <ShortName>${host.siteName}</ShortName>
+  <Description>Search in ${host.siteName}</Description>
+  <InputEncoding>UTF-8</InputEncoding>
+  <Image width="16" height="16" type="image/x-icon">https://${host.host}/favicon.ico</Image>
+  <Url type="text/html" method="get" template="https://${host.host}/search?q={searchTerms}&amp;ref=opensearch"/>
+  <moz:SearchForm>https://${host.host}/search</moz:SearchForm>
+</OpenSearchDescription>`;
+}
+
+export const getServerSideProps: GetServerSideProps<{}> = ({ res, req }) => {
+  res.setHeader("Content-Type", "text/xml");
+  res.write(Opensearch(getSiteConfig(req.headers.host)));
+  res.end();
+
+  return Promise.resolve({
+    props: {},
+  });
+};

+ 12 - 9
styles/globals.scss

@@ -161,15 +161,6 @@
       }
     }
   }
-  .search-input {
-    @apply relative mx-5 my-2 md:my-0;
-    .icon-search {
-      @apply absolute left-2 h-full w-5 fill-sky-500;
-    }
-    input {
-      @apply block w-full border border-sky-500 rounded-lg pl-10 pr-4 py-1 focus:outline-none bg-transparent; // border-primary text-primary;
-    }
-  }
   .buttons {
     @apply flex;
   }
@@ -228,6 +219,18 @@
   // .breadcrumbs {
   //   @apply hidden lg:flex;
   // }
+  .search-input {
+    @apply mx-5 my-2 md:my-0;
+  }
+}
+.search-input {
+  @apply relative;
+  .icon-search {
+    @apply absolute left-2 h-full w-5 fill-sky-500;
+  }
+  input {
+    @apply block w-full border border-sky-500 rounded-lg pl-10 pr-4 py-1 focus:outline-none bg-transparent; // border-primary text-primary;
+  }
 }
 .novel-title {
   @apply mt-5 mb-2 flex justify-between text-2xl sm:text-3xl font-extrabold text-gray-900 tracking-tight dark:text-gray-200;

+ 18 - 1
types/http.d.ts

@@ -4,6 +4,15 @@ declare interface ResData<T> {
   errno: number;
 }
 
+declare interface ResPageData<T> {
+  pageSize: number;
+  pageIndex: number;
+  sort: string;
+  total_rows: number;
+  total_pages: number;
+  rows: T[];
+}
+
 declare interface GenreItem {
   id: number;
   name: string;
@@ -32,10 +41,12 @@ declare interface ListItem {
   genre: string;
   img: string;
   name: string;
-  stauts: number;
+  stauts: string;
   uri: string;
 }
 
+declare type NovelList = ResPageData<ListItem>;
+
 declare interface ChapterItem {
   id: number;
   novel_id: number;
@@ -61,3 +72,9 @@ declare interface ChapterData {
   pre: string;
   title: string;
 }
+
+declare interface HomeData {
+  completed: ListItem[];
+  on_going: ListItem[];
+  rands: ListItem[];
+}