Repos / pytaku / 9f2b32c335
commit 9f2b32c3358f29b2780fd78c7f9707c219293dda
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Tue Aug 18 23:02:46 2020 +0700

    mithril "follows" route

diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index b73dc0e..0791098 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -435,4 +435,10 @@ def api_logout():
 @app.route("/api/follows", methods=["GET"])
 @require_token
 def api_follows():
-    return jsonify({"message": "TODO"})
+    titles = get_followed_titles(request.user_id)
+    for title in titles:
+        thumbnail = title_thumbnail(title["site"], title["id"])
+        if title["site"] == "mangadex":
+            thumbnail = url_for("proxy_view", b64_url=_encode_proxy_url(thumbnail))
+        title["thumbnail"] = thumbnail
+    return jsonify({"titles": titles})
diff --git a/src/pytaku/static/js/routes/follows.js b/src/pytaku/static/js/routes/follows.js
index cd4e18f..06679fc 100644
--- a/src/pytaku/static/js/routes/follows.js
+++ b/src/pytaku/static/js/routes/follows.js
@@ -1,29 +1,112 @@
 import { Auth } from "../models.js";
 
+const truncate = (input) =>
+  input.length > 20 ? `${input.substring(0, 20)}...` : input;
+
+function fullChapterName(chapter) {
+  let result = "Chapter " + chapter.num_major;
+  if (chapter.num_minor) {
+    result += "." + chapter.num_minor;
+  }
+  if (chapter.volume) {
+    result += " Volume " + chapter.volume;
+  }
+  if (chapter.name) {
+    result += " - " + chapter.name;
+  }
+  return result;
+}
+
+const Title = {
+  view: (vnode) => {
+    const title = vnode.attrs.title;
+    const numChaptersToDisplay = 4;
+
+    return m(
+      "div.follows--title" + (title.chapters.length === 0 ? ".empty" : ""),
+      [
+        m("div", [
+          m(m.route.Link, { href: `/m/${title.site}/${title.id}` }, [
+            m("img.follows--cover", { src: title.thumbnail, alt: title.name }),
+          ]),
+        ]),
+        m("div.follows--chapters", [
+          title.chapters.length > numChaptersToDisplay
+            ? m(
+                m.route.Link,
+                {
+                  href: `/m/${title.site}/${title.id}`,
+                  class: "follows--chapter follows--more",
+                },
+                `and ${title.chapters.length - numChaptersToDisplay} more...`
+              )
+            : "",
+          title.chapters.slice(-numChaptersToDisplay).map((chapter) =>
+            m(
+              m.route.Link,
+              {
+                href: `/m/${title.site}/${title.id}/${chapter.id}`,
+                class: "follows--chapter",
+              },
+              [
+                fullChapterName(chapter),
+                chapter.groups.map((group) => {
+                  m("span.follows--group", truncate(group));
+                }),
+              ]
+            )
+          ),
+        ]),
+      ]
+    );
+  },
+};
+
 function Follows(initialVNode) {
   let titles = [];
+  let isLoading = false;
+
   return {
     oninit: () => {
+      isLoading = true;
       Auth.request({
         method: "GET",
         url: "/api/follows",
-      });
+      })
+        .then((resp) => {
+          titles = resp.titles;
+        })
+        .catch((err) => {
+          alert("TODO");
+          console.log(err);
+        })
+        .finally(() => {
+          isLoading = false;
+        });
     },
+
     oncreate: (vnode) => {
       document.title = "Stuff I follow - Pytaku";
     },
+
     view: (vnode) => {
-      return m("div.content", [
-        titles.map((title) =>
-          m("div.title", [
-            m("div", [
-              m("a", { href: "TODO" }, [
-                m("img.cover", { src: title.thumbnail, alt: title.name }),
-              ]),
-            ]),
-          ])
-        ),
-      ]);
+      let content = "";
+
+      if (isLoading) {
+        return m(
+          "div.content",
+          m("h2.blink", [m("i.icon.icon-loader.spin"), " loading..."])
+        );
+      }
+
+      if (titles.length === 0) {
+        return m(
+          "div.content",
+          "You're not following any title yet. Try searching for some."
+        );
+      }
+
+      return m("div.content", [titles.map((title) => m(Title, { title }))]);
     },
   };
 }
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index ec92e72..4d9eec0 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -1,3 +1,41 @@
+/* --- Utilities --- */
+
+/* spinner */
+.spin {
+  display: inline-block;
+  animation-name: spin;
+  animation-duration: 1s;
+  animation-iteration-count: infinite;
+  animation-timing-function: linear;
+}
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* blinker */
+.blink {
+  display: inline-block;
+  animation-name: blink;
+  animation-duration: 700ms;
+  animation-iteration-count: infinite;
+  animation-timing-function: linear;
+}
+@keyframes blink {
+  from {
+    opacity: 0.2;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+/* --- End Utilities --- */
+
 /* Navbar */
 nav {
   background-color: var(--bg-black);
@@ -121,3 +159,64 @@ .auth--form--message-success {
 .auth--form--message-error {
   color: red;
 }
+
+/* Follows route */
+
+.follows--title {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  margin-bottom: var(--body-padding);
+  background-color: #efefef;
+}
+.follows--title.empty {
+  display: inline-flex;
+}
+.follows--title.empty .chapters {
+  display: none;
+}
+
+.follows--cover {
+  border: 1px solid #777;
+  margin-right: 0.5rem;
+  max-width: 150px;
+}
+.follows--cover:hover {
+  box-shadow: 0 0 3px black;
+}
+
+.follows--chapters {
+  padding: 0.5rem 0.5rem 0.5rem 0;
+}
+
+.follows--chapter {
+  display: block;
+  margin-bottom: 0.5rem;
+  background-color: white;
+  border: 1px solid #999;
+  padding: 6px;
+  border-radius: 5px;
+  text-decoration: none;
+  color: black;
+}
+.follows--chapter:hover {
+  background-color: #eee;
+}
+.follows--chapter:last-child::after {
+  content: "← resume here";
+  background-color: cornsilk;
+  white-space: nowrap;
+}
+
+.follows--group {
+  font-size: 0.9em;
+  background-color: #ddd;
+  border-radius: 3px;
+  white-space: nowrap;
+  padding: 2px 5px;
+}
+
+.follows--more {
+  display: inline-block;
+  font-style: italic;
+}