Repos / pytaku / 61300c25af
commit 61300c25af2d34b68b83e825169acd62854489d6
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Fri Aug 21 22:52:13 2020 +0700

    implement mithril search

diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 0791098..29b5d01 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -366,7 +366,9 @@ def ensure_titles(site_title_pairs: List[Tuple[str, str]]):
 @app.route("/h")
 @app.route("/a")
 @app.route("/f")
-def home_view():
+@app.route("/s")
+@app.route("/s/<query>")
+def home_view(query=None):
     return render_template("spa.html")
 
 
@@ -442,3 +444,15 @@ def api_follows():
             thumbnail = url_for("proxy_view", b64_url=_encode_proxy_url(thumbnail))
         title["thumbnail"] = thumbnail
     return jsonify({"titles": titles})
+
+
+@app.route("/api/search/<query>", methods=["GET"])
+def api_search(query):
+    results = search_title_all_sites(query)
+
+    if "mangadex" in results:
+        for title in results["mangadex"]:
+            title["thumbnail"] = url_for(
+                "proxy_view", b64_url=_encode_proxy_url(title["thumbnail"])
+            )
+    return results
diff --git a/src/pytaku/static/js/layout.js b/src/pytaku/static/js/layout.js
index 7807b0f..7698c14 100644
--- a/src/pytaku/static/js/layout.js
+++ b/src/pytaku/static/js/layout.js
@@ -1,7 +1,8 @@
-import { Auth } from "./models.js";
+import { Auth, SearchModel } from "./models.js";
 
 function Navbar(initialVNode) {
   let isLoggingOut = false;
+
   return {
     view: (vnode) => {
       let userLink;
@@ -50,10 +51,24 @@ function Navbar(initialVNode) {
             }),
           ]
         ),
-        m("form.nav--search-form", [
-          m("input", { placeholder: "search title name" }),
-          m("button", { type: "submit" }, [m("i.icon.icon-search")]),
-        ]),
+        m(
+          "form.nav--search-form",
+          {
+            onsubmit: (ev) => {
+              ev.preventDefault();
+              m.route.set("/s/:query", { query: SearchModel.query });
+            },
+          },
+          [
+            m("input[placeholder=search manga title]", {
+              onchange: (ev) => {
+                SearchModel.query = ev.target.value;
+              },
+              value: SearchModel.query,
+            }),
+            m("button[type=submit]", [m("i.icon.icon-search")]),
+          ]
+        ),
         userLink,
       ]);
     },
diff --git a/src/pytaku/static/js/main.js b/src/pytaku/static/js/main.js
index 2dde99a..7863e87 100644
--- a/src/pytaku/static/js/main.js
+++ b/src/pytaku/static/js/main.js
@@ -3,6 +3,7 @@ import Layout from "./layout.js";
 import Authentication from "./routes/authentication.js";
 import Home from "./routes/home.js";
 import Follows from "./routes/follows.js";
+import Search from "./routes/search.js";
 
 Auth.init().then(() => {
   const root = document.getElementById("spa-root");
@@ -16,7 +17,7 @@ Auth.init().then(() => {
           return Home;
         }
       },
-      render: () => m(Layout, m(Home)),
+      render: (vnode) => m(Layout, vnode),
     },
     "/a": {
       onmatch: () => {
@@ -26,18 +27,30 @@ Auth.init().then(() => {
           return Authentication;
         }
       },
-      render: () => m(Layout, m(Authentication)),
+      render: (vnode) => m(Layout, vnode),
     },
     "/f": {
       onmatch: () => {
         if (Auth.isLoggedIn()) {
           return Follows;
         } else {
-          //m.route.set("/a", null, { replace: true });
-          return m("h1", "waiting");
+          m.route.set("/a", null, { replace: true });
         }
       },
-      render: () => m(Layout, m(Follows)),
+      render: (vnode) => m(Layout, vnode),
+    },
+    "/s/:query": {
+      render: (vnode) =>
+        m(
+          Layout,
+          m(Search, {
+            query: vnode.attrs.query,
+            key: vnode.attrs.query,
+            // ^ set a key here to reinitialize Search component on route
+            // change. Without it, Search.oninit would only trigger once on
+            // first full page load.
+          })
+        ),
     },
   });
 });
diff --git a/src/pytaku/static/js/models.js b/src/pytaku/static/js/models.js
index e1dc2b4..0159684 100644
--- a/src/pytaku/static/js/models.js
+++ b/src/pytaku/static/js/models.js
@@ -89,4 +89,35 @@ const Auth = {
   },
 };
 
-export { Auth };
+const SearchModel = {
+  query: "",
+  result: {},
+  cache: {},
+  isLoading: true,
+  performSearch: (query) => {
+    SearchModel.query = query;
+    if (SearchModel.cache[query]) {
+      SearchModel.result = SearchModel.cache[query];
+    } else {
+      SearchModel.isLoading = true;
+      m.redraw();
+      m.request({
+        method: "GET",
+        url: "/api/search/:query",
+        params: { query },
+      })
+        .then((resp) => {
+          SearchModel.cache[query] = resp;
+          SearchModel.result = resp;
+        })
+        .catch((err) => {
+          console.log("TODO", err);
+        })
+        .finally(() => {
+          SearchModel.isLoading = false;
+        });
+    }
+  },
+};
+
+export { Auth, SearchModel };
diff --git a/src/pytaku/static/js/routes/follows.js b/src/pytaku/static/js/routes/follows.js
index 06679fc..7097eec 100644
--- a/src/pytaku/static/js/routes/follows.js
+++ b/src/pytaku/static/js/routes/follows.js
@@ -1,7 +1,5 @@
 import { Auth } from "../models.js";
-
-const truncate = (input) =>
-  input.length > 20 ? `${input.substring(0, 20)}...` : input;
+import { LoadingMessage, truncate } from "../utils.js";
 
 function fullChapterName(chapter) {
   let result = "Chapter " + chapter.num_major;
@@ -51,7 +49,7 @@ const Title = {
               [
                 fullChapterName(chapter),
                 chapter.groups.map((group) => {
-                  m("span.follows--group", truncate(group));
+                  m("span.follows--group", truncate(group, 20));
                 }),
               ]
             )
@@ -93,10 +91,7 @@ function Follows(initialVNode) {
       let content = "";
 
       if (isLoading) {
-        return m(
-          "div.content",
-          m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."])
-        );
+        return m("div.content", m(LoadingMessage));
       }
 
       if (titles.length === 0) {
diff --git a/src/pytaku/static/js/routes/search.js b/src/pytaku/static/js/routes/search.js
new file mode 100644
index 0000000..6551504
--- /dev/null
+++ b/src/pytaku/static/js/routes/search.js
@@ -0,0 +1,51 @@
+import { Auth, SearchModel } from "../models.js";
+import { LoadingMessage, truncate } from "../utils.js";
+
+const Search = {
+  oninit: (vnode) => {
+    document.title = `"${vnode.attrs.query}" search results`;
+    SearchModel.performSearch(vnode.attrs.query);
+  },
+  view: (vnode) => {
+    return m(
+      "div.content",
+      SearchModel.isLoading
+        ? m(LoadingMessage)
+        : Object.entries(SearchModel.result).map(([site, titles]) =>
+            m("div", [
+              m("h1.search--site-heading", site),
+              titles
+                ? m("p.search--result-text", [
+                    "Showing ",
+                    m("strong", titles.length),
+                    ` result${titles.length > 1 ? "s" : ""} for `,
+                    SearchModel.query,
+                  ])
+                : m(
+                    "p.search--result-text",
+                    `No results for "${SearchModel.query}"`
+                  ),
+              m(
+                "div.search--results",
+                titles.map((title) =>
+                  m(
+                    m.route.Link,
+                    {
+                      class: "search--result",
+                      href: `/m/${site}/${title.id}`,
+                      title: title.name,
+                    },
+                    [
+                      m("img", { src: title.thumbnail, alt: title.name }),
+                      m("span", truncate(title.name, 50)),
+                    ]
+                  )
+                )
+              ),
+            ])
+          )
+    );
+  },
+};
+
+export default Search;
diff --git a/src/pytaku/static/js/utils.js b/src/pytaku/static/js/utils.js
new file mode 100644
index 0000000..bf0105a
--- /dev/null
+++ b/src/pytaku/static/js/utils.js
@@ -0,0 +1,8 @@
+const LoadingMessage = {
+  view: (vnode) => m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."]),
+};
+
+const truncate = (input, size) =>
+  input.length > size ? `${input.substring(0, size)}...` : input;
+
+export { LoadingMessage, truncate };
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index 4d9eec0..a1db482 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -220,3 +220,32 @@ .follows--more {
   display: inline-block;
   font-style: italic;
 }
+
+/* Search route */
+.search--site-heading {
+  text-transform: capitalize;
+}
+.search--results {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+.search--result {
+  display: inline-flex;
+  flex-direction: column;
+  border: 1px solid var(--bg-black);
+  margin: 0 1rem 1rem 0;
+  background-color: var(--bg-black);
+  color: white;
+  text-decoration: none;
+  max-width: 150px;
+}
+.search--result span {
+  padding: 0.5rem;
+  width: 0;
+  min-width: 100%;
+}
+
+.search--result-text {
+  margin-bottom: 1rem;
+}