Repos / pytaku / 36c887b857
commit 36c887b85712c06b63f00bc05899b77eb47b3e5b
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Fri Jul 31 16:55:16 2020 +0700

    basic templates, navbar & buttons

diff --git a/README.md b/README.md
index 4631d69..bccf335 100644
--- a/README.md
+++ b/README.md
@@ -4,4 +4,6 @@
 pip install https://github.com/rogerbinns/apsw/releases/download/3.32.2-r1/apsw-3.32.2-r1.zip \
       --global-option=fetch --global-option=--version --global-option=3.32.2 --global-option=--all \
       --global-option=build --global-option=--enable-all-extensions
+
+FLASK_ENV=development FLASK_APP=pytaku.main:app flask run
 ```
diff --git a/poetry.lock b/poetry.lock
index ed9ba38..4654731 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,8 +1,226 @@
-package = []
+[[package]]
+category = "main"
+description = "Python package for providing Mozilla's CA Bundle."
+name = "certifi"
+optional = false
+python-versions = "*"
+version = "2020.6.20"
+
+[[package]]
+category = "main"
+description = "Universal encoding detector for Python 2 and 3"
+name = "chardet"
+optional = false
+python-versions = "*"
+version = "3.0.4"
+
+[[package]]
+category = "main"
+description = "Composable command line interface toolkit"
+name = "click"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "7.1.2"
+
+[[package]]
+category = "main"
+description = "A simple framework for building complex web applications."
+name = "flask"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "1.1.2"
+
+[package.dependencies]
+Jinja2 = ">=2.10.1"
+Werkzeug = ">=0.15"
+click = ">=5.1"
+itsdangerous = ">=0.24"
+
+[package.extras]
+dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
+docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
+dotenv = ["python-dotenv"]
+
+[[package]]
+category = "main"
+description = "WSGI HTTP Server for UNIX"
+name = "gunicorn"
+optional = false
+python-versions = ">=3.4"
+version = "20.0.4"
+
+[package.dependencies]
+setuptools = ">=3.0"
+
+[package.extras]
+eventlet = ["eventlet (>=0.9.7)"]
+gevent = ["gevent (>=0.13)"]
+setproctitle = ["setproctitle"]
+tornado = ["tornado (>=0.2)"]
+
+[[package]]
+category = "main"
+description = "Internationalized Domain Names in Applications (IDNA)"
+name = "idna"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.10"
+
+[[package]]
+category = "main"
+description = "Various helpers to pass data to untrusted environments and back."
+name = "itsdangerous"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.1.0"
+
+[[package]]
+category = "main"
+description = "A very fast and expressive template engine."
+name = "jinja2"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.11.2"
+
+[package.dependencies]
+MarkupSafe = ">=0.23"
+
+[package.extras]
+i18n = ["Babel (>=0.8)"]
+
+[[package]]
+category = "main"
+description = "Safely add untrusted strings to HTML/XML markup."
+name = "markupsafe"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+version = "1.1.1"
+
+[[package]]
+category = "main"
+description = "Python HTTP for Humans."
+name = "requests"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.24.0"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+chardet = ">=3.0.2,<4"
+idna = ">=2.5,<3"
+urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
+
+[package.extras]
+security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
+
+[[package]]
+category = "main"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+name = "urllib3"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+version = "1.25.10"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
+
+[[package]]
+category = "main"
+description = "The comprehensive WSGI web application library."
+name = "werkzeug"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "1.0.1"
+
+[package.extras]
+dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
+watchdog = ["watchdog"]
 
 [metadata]
-content-hash = "669741988c507fb04697bdb0c9077fa1b2342c356df6ae6c96baa3119a96a9ea"
+content-hash = "c1d5cedfcf8897fb539d7bbd395966ad5c864b9a57737ae54e9ae099e9600026"
 lock-version = "1.0"
 python-versions = "^3.7"
 
 [metadata.files]
+certifi = [
+    {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
+    {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
+]
+chardet = [
+    {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
+    {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
+]
+click = [
+    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
+    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
+]
+flask = [
+    {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
+    {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
+]
+gunicorn = [
+    {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
+    {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
+]
+idna = [
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+]
+itsdangerous = [
+    {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
+    {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
+]
+jinja2 = [
+    {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
+    {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
+]
+markupsafe = [
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
+    {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
+]
+requests = [
+    {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
+    {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
+]
+urllib3 = [
+    {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"},
+    {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"},
+]
+werkzeug = [
+    {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
+    {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index 8f64077..cf9cb6b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,12 +4,19 @@ version = "0.1.1"
 description = ""
 authors = ["Bùi Thành Nhân <hi@imnhan.com>"]
 license = "AGPL-3.0-only"
+packages = [
+    { include = "pytaku", from = "src" },
+    { include = "mangoapi", from = "src" },
+]
 
 [tool.poetry.scripts]
 pytaku-migrate = "pytaku:migrate"
 
 [tool.poetry.dependencies]
 python = "^3.7"
+flask = "^1.1.2"
+gunicorn = "^20.0.4"
+requests = "^2.24.0"
 
 [tool.poetry.dev-dependencies]
 
diff --git a/src/mangoapi/__init__.py b/src/mangoapi/__init__.py
new file mode 100644
index 0000000..a9870a4
--- /dev/null
+++ b/src/mangoapi/__init__.py
@@ -0,0 +1,45 @@
+import requests
+
+
+def _parse_chapter_number(string):
+    nums = string.split(".")
+    count = len(nums)
+    assert count == 1 or count == 2
+    result = {"number": string}
+    result["major"] = int(nums[0])
+    if count == 2:
+        result["minor"] = int(nums[1])
+    return result
+
+
+def get_title(title_id):
+    md_resp = requests.get(f"https://mangadex.org/api/?id={title_id}&type=manga")
+    assert md_resp.status_code == 200
+    md_json = md_resp.json()
+    assert md_json["status"] == "OK"
+
+    cover_url = md_json["manga"]["cover_url"]
+    cover = "https://mangadex.org" + cover_url[: cover_url.rfind("?")]
+
+    title = {
+        "name": md_json["manga"]["title"],
+        "alt_names": md_json["manga"]["alt_names"],
+        "cover": cover,
+        "descriptions": md_json["manga"]["description"].split("\r\n\r\n"),
+        "chapters": [
+            {
+                "id": chap_id,
+                "name": chap["title"],
+                "volume": int(chap["volume"]) if chap["volume"] else None,
+                "group": chap["group_name"],
+                **_parse_chapter_number(chap["chapter"]),
+            }
+            for chap_id, chap in md_json["chapter"].items()
+            if chap["lang_code"] == "gb"
+        ],
+    }
+    return title
+
+
+def get_chapter(chapter_id):
+    return {"id": chapter_id}
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
new file mode 100644
index 0000000..dc59158
--- /dev/null
+++ b/src/pytaku/main.py
@@ -0,0 +1,22 @@
+from flask import Flask, render_template
+
+from mangoapi import get_chapter, get_title
+
+app = Flask(__name__)
+
+
+@app.route("/title/mangadex/<int:title_id>")
+def title_view(title_id):
+    title = get_title(title_id)
+    return render_template("title.html", **title)
+
+
+@app.route("/chapter/mangadex/<int:chapter_id>")
+def chapter_view(chapter_id):
+    chapter = get_chapter(chapter_id)
+    return render_template("chapter.html", **chapter)
+
+
+@app.route("/search")
+def search_view():
+    return "TODO"
diff --git a/src/pytaku/static/base.css b/src/pytaku/static/base.css
new file mode 100644
index 0000000..5c3a823
--- /dev/null
+++ b/src/pytaku/static/base.css
@@ -0,0 +1,151 @@
+/* Look & feel */
+:root {
+  --btn-red: #ef4f39;
+  --btn-red-bottom: #b94434;
+  --btn-green: #3ba60a;
+  --btn-green-bottom: #338a0d;
+  --btn-blue: #009ee8;
+  --btn-blue-bottom: #26789f;
+  --bg-black: #231f20;
+  --border-radius: 3px;
+  --body-padding: 0.5rem;
+
+  font-size: 100%;
+  font-family: "Ubuntu", sans-serif;
+  overflow-y: scroll;
+}
+
+h1 {
+  margin-top: 1rem;
+  margin-bottom: 0.5rem;
+  font-size: 2rem;
+  font-weight: bold;
+}
+
+table {
+  border-collapse: collapse;
+}
+td,
+th {
+  border: 1px solid black;
+  padding: 0.5em;
+}
+tr:nth-child(even) {
+  background-color: #eee;
+}
+th {
+  background-color: #777;
+  color: white;
+}
+
+input {
+  padding: 0.3rem;
+}
+
+.button {
+  display: inline-block;
+  user-select: none;
+  margin: 0;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 0.3rem;
+
+  cursor: pointer;
+  border: 0;
+  padding: 0.5rem 1rem 0.3rem 1rem;
+  border-radius: var(--border-radius);
+  color: white;
+  text-decoration: none;
+}
+.button:hover {
+  filter: brightness(110%);
+}
+.button:active {
+  filter: brightness(90%);
+}
+.button:focus {
+  box-shadow: 0 0 2px black;
+}
+.button.red {
+  background-color: var(--btn-red);
+  border-bottom: 4px solid var(--btn-red-bottom);
+}
+.button.green {
+  background-color: var(--btn-green);
+  border-bottom: 4px solid var(--btn-green-bottom);
+}
+.button.blue {
+  background-color: var(--btn-blue);
+  border-bottom: 4px solid var(--btn-blue-bottom);
+}
+.button > img {
+  height: 1rem;
+}
+
+/* Grid layout */
+
+nav {
+  display: grid;
+  grid-gap: 0.5rem;
+  grid-template-areas: "logo search links";
+  padding: var(--body-padding);
+}
+
+.logo {
+  grid-area: logo;
+  width: 150px;
+}
+.logo img {
+  max-width: 100%;
+  display: block;
+}
+
+.search {
+  grid-area: search;
+  display: flex;
+}
+
+.links {
+  grid-area: links;
+  display: inline-flex;
+  gap: 1rem;
+  justify-content: center;
+  align-items: center;
+}
+.links a * {
+  vertical-align: middle;
+}
+
+/* Component-specific styling */
+
+nav {
+  background-color: var(--bg-black);
+}
+
+.search input {
+  border: 0;
+  border-radius: var(--border-radius) 0 0 var(--border-radius);
+}
+.search button {
+  border-radius: 0 var(--border-radius) var(--border-radius) 0;
+}
+
+.links a {
+  color: white;
+  text-decoration: none;
+}
+.links a:hover {
+  text-decoration: underline;
+}
+
+.content {
+  margin: auto;
+  max-width: 800px;
+  padding: var(--body-padding);
+}
+.content > * {
+  display: block;
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
diff --git a/src/pytaku/static/icons/LICENSE b/src/pytaku/static/icons/LICENSE
new file mode 100644
index 0000000..29e925b
--- /dev/null
+++ b/src/pytaku/static/icons/LICENSE
@@ -0,0 +1,24 @@
+Feather icon set by Cole Bemis:
+https://github.com/feathericons/feather
+
+The MIT License (MIT)
+
+Copyright (c) 2013-2017 Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/pytaku/static/icons/bookmark.svg b/src/pytaku/static/icons/bookmark.svg
new file mode 100644
index 0000000..ce23590
--- /dev/null
+++ b/src/pytaku/static/icons/bookmark.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bookmark"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/chevrons-left.svg b/src/pytaku/static/icons/chevrons-left.svg
new file mode 100644
index 0000000..3b96532
--- /dev/null
+++ b/src/pytaku/static/icons/chevrons-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-left"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/chevrons-right.svg b/src/pytaku/static/icons/chevrons-right.svg
new file mode 100644
index 0000000..7071dfc
--- /dev/null
+++ b/src/pytaku/static/icons/chevrons-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/eye.svg b/src/pytaku/static/icons/eye.svg
new file mode 100644
index 0000000..1835ad0
--- /dev/null
+++ b/src/pytaku/static/icons/eye.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/search.svg b/src/pytaku/static/icons/search.svg
new file mode 100644
index 0000000..c478d69
--- /dev/null
+++ b/src/pytaku/static/icons/search.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/user.svg b/src/pytaku/static/icons/user.svg
new file mode 100644
index 0000000..40cd42f
--- /dev/null
+++ b/src/pytaku/static/icons/user.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/minireset.css b/src/pytaku/static/minireset.css
new file mode 100644
index 0000000..68cd6df
--- /dev/null
+++ b/src/pytaku/static/minireset.css
@@ -0,0 +1,75 @@
+/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
+html,
+body,
+p,
+ol,
+ul,
+li,
+dl,
+dt,
+dd,
+blockquote,
+figure,
+fieldset,
+legend,
+textarea,
+pre,
+iframe,
+hr,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  margin: 0;
+  padding: 0;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: 100%;
+  font-weight: normal;
+}
+ul {
+  list-style: none;
+}
+button,
+input,
+select,
+textarea {
+  margin: 0;
+  font-family: inherit;
+  font-size: inherit;
+}
+html {
+  box-sizing: border-box;
+}
+*,
+*::before,
+*::after {
+  box-sizing: inherit;
+}
+img,
+video {
+  height: auto;
+  max-width: 100%;
+}
+iframe {
+  border: 0;
+}
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+td,
+th {
+  padding: 0;
+}
+td:not([align]),
+th:not([align]) {
+  text-align: left;
+}
diff --git a/src/pytaku/static/pytaku.svg b/src/pytaku/static/pytaku.svg
new file mode 100644
index 0000000..3445fc2
--- /dev/null
+++ b/src/pytaku/static/pytaku.svg
@@ -0,0 +1,394 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="206.37485mm"
+   height="57.462528mm"
+   viewBox="0 0 206.37485 57.462528"
+   version="1.1"
+   id="svg706"
+   sodipodi:docname="white.svg"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14">
+  <defs
+     id="defs700">
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath154">
+      <path
+         d="M 0,2722.167 H 1600 V 0 H 0 Z"
+         id="path152"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath222">
+      <path
+         d="m 1082.03,1602.18 h 152.63 v -117.4 h -152.63 z"
+         id="path220"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath266">
+      <path
+         d="m 1158.34,1598.11 h 70.45 v -109.18 h -70.45 z"
+         id="path264"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.49497475"
+     inkscape:cx="9.5170841"
+     inkscape:cy="290.97019"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1015"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata703">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-1.5822303,-132.49581)">
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,85.180821,80.125437)"
+       id="g566"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path564"
+         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 9.363,0 14.835,5.594 14.835,12.89 v 0.243 c 0,8.39 -5.837,12.89 -15.2,12.89 h -14.47 L -14.835,0 Z M -33.562,42.925 H 1.216 c 20.307,0 32.589,-12.038 32.589,-29.427 v -0.244 c 0,-19.699 -15.322,-29.914 -34.413,-29.914 h -14.227 v -25.536 h -18.727 z" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,99.723701,100.41644)"
+       id="g570"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path568"
+         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 6.202,13.376 c 2.432,-1.459 5.472,-2.553 7.904,-2.553 3.161,0 4.864,0.972 6.445,4.256 L -4.986,80.5 H 14.592 L 29.428,36.116 43.655,80.5 H 62.868 L 37.818,13.741 C 32.832,0.487 27.482,-4.499 16.416,-4.499 9.728,-4.499 4.742,-2.797 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,125.11954,88.490637)"
+       id="g574"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path572"
+         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 V 30.887 H -7.783 V 46.695 H 0 V 63.354 H 18.483 V 46.695 H 33.805 V 30.887 H 18.483 V 3.04 c 0,-4.256 1.824,-6.323 5.959,-6.323 3.405,0 6.445,0.851 9.12,2.31 v -14.835 c -3.891,-2.311 -8.391,-3.77 -14.592,-3.77 C 7.661,-19.578 0,-15.079 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,153.73306,86.903387)"
+       id="g578"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path576"
+         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 v 3.283 c -3.162,1.459 -7.296,2.432 -11.795,2.432 -7.904,0 -12.769,-3.162 -12.769,-8.998 v -0.244 c 0,-4.985 4.135,-7.904 10.093,-7.904 C -5.837,-11.431 0,-6.688 0,0 m -42.439,-4.256 v 0.243 c 0,14.228 10.822,20.794 26.266,20.794 6.566,0 11.309,-1.094 15.93,-2.675 V 15.2 c 0,7.661 -4.743,11.917 -13.984,11.917 -7.053,0 -12.039,-1.337 -17.997,-3.526 l -4.621,14.106 c 7.174,3.161 14.227,5.228 25.293,5.228 10.093,0 17.389,-2.675 22.01,-7.296 4.864,-4.864 7.053,-12.038 7.053,-20.794 V -22.983 H -0.365 v 7.053 c -4.499,-4.986 -10.701,-8.269 -19.699,-8.269 -12.282,0 -22.375,7.053 -22.375,19.943" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,163.8145,63.695337)"
+       id="g582"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path580"
+         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 h 18.483 v -47.303 l 21.646,23.712 H 62.26 L 37.453,-49.249 63.111,-88.769 H 41.953 l -17.025,26.63 -6.445,-6.809 V -88.769 H 0 Z" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,186.89413,86.903387)"
+       id="g586"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path584"
+         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 V 42.196 H 18.483 V 5.837 c 0,-8.755 4.135,-13.255 11.188,-13.255 7.053,0 11.552,4.5 11.552,13.255 V 42.196 H 59.706 V -22.983 H 41.223 v 9.242 C 36.967,-19.213 31.495,-24.199 22.131,-24.199 8.147,-24.199 0,-14.957 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,53.298392,51.245877)"
+       id="g590"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path588"
+         style="fill:#ef3d26;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 h -130.308 c -8.959,0 -16.289,-7.33 -16.289,-16.288 v -35.809 -23.906 -70.594 c 0,-8.959 7.33,-16.288 16.289,-16.288 h 30.653 l -14.241,26.358 h -0.124 v 0.229 84.201 25.738 h 52.72 c 18,0 33.649,-10.53 40.827,-25.854 l 36.762,-68.039 V -16.288 C 16.289,-7.33 8.959,0 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,59.044647,102.96201)"
+       id="g594"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path592"
+         style="fill:#b22a24;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 v 26.345 l -36.762,68.039 c 2.677,-5.714 4.185,-12.091 4.185,-18.843 0,-24.4 -19.985,-44.544 -44.538,-44.544 V 10.07 h -53.07 l 14.242,-26.359 h 99.654 C -7.33,-16.289 0,-8.959 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,46.075974,69.665327)"
+       id="g598"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path596"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c -7.178,15.324 -22.827,25.854 -40.827,25.854 h -52.72 V -84.085 l 0.124,-0.229 h 53.07 v 20.926 c 24.553,0 44.537,20.145 44.537,44.544 C 4.184,-12.091 2.677,-5.715 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,20.92313,70.010027)"
+       id="g602"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path600"
+         style="fill:#bf3326;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 13.489,-6.405 c 1.521,-0.72 2.442,-1.841 2.442,-3.402 v -0.4 c 0,-1.561 -0.921,-2.682 -2.442,-3.403 L 0,-20.014 c -0.48,-0.24 -0.921,-0.4 -1.481,-0.4 -1.281,-0.04 -2.442,1.081 -2.442,2.521 0,1.161 0.641,1.962 1.681,2.442 l 12.089,5.404 -12.089,5.404 c -1.04,0.48 -1.681,1.401 -1.681,2.522 0,1.481 1.161,2.521 2.642,2.481 C -0.841,0.36 -0.4,0.2 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,39.703783,77.070487)"
+       id="g606"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path604"
+         style="fill:#bf3326;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 -13.489,6.404 c -1.521,0.721 -2.442,1.842 -2.442,3.403 v 0.4 c 0,1.561 0.921,2.682 2.442,3.402 L 0,20.014 c 0.48,0.24 0.921,0.4 1.481,0.4 1.281,0.04 2.442,-1.081 2.442,-2.521 0,-1.161 -0.641,-1.962 -1.681,-2.442 L -9.847,10.047 2.242,4.643 C 3.282,4.163 3.923,3.242 3.923,2.121 3.923,0.64 2.762,-0.4 1.281,-0.36 0.841,-0.36 0.4,-0.2 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,34.683333,78.139577)"
+       id="g610"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path608"
+         style="fill:#bf3326;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c -1.393,0 -2.543,-1.15 -2.543,-2.543 h 0.003 c 0,-3.042 -1.521,-4.844 -3.483,-4.844 h -0.08 c -2.161,0 -3.442,2.042 -3.442,4.844 h 0.004 c 0,1.558 -1.285,2.844 -2.844,2.844 -1.558,0 -2.844,-1.286 -2.844,-2.844 0,-2.802 -1.281,-4.844 -3.442,-4.844 h -0.08 c -1.962,0 -3.483,1.802 -3.483,4.844 h 0.003 c 0,1.393 -1.15,2.543 -2.543,2.543 -1.394,0 -2.544,-1.15 -2.544,-2.543 0,-6.205 3.043,-10.808 8.246,-10.808 h 0.08 c 3.323,0 5.124,1.681 6.605,4.523 1.481,-2.842 3.282,-4.523 6.605,-4.523 h 0.08 c 5.203,0 8.245,4.603 8.245,10.808 C 2.543,-1.15 1.394,0 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,13.074706,99.409427)"
+       id="g614"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path612"
+         style="fill:#424143;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 H 0.124 L 0,0.229 Z" />
+    </g>
+    <g
+       style="display:inline;fill:#ffffff;fill-opacity:1"
+       transform="matrix(0.35277777,0,0,-0.35277777,85.180823,161.37537)"
+       id="g566-7"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path564-2"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 9.363,0 14.835,5.594 14.835,12.89 v 0.243 c 0,8.39 -5.837,12.89 -15.2,12.89 h -14.47 L -14.835,0 Z M -33.562,42.925 H 1.216 c 20.307,0 32.589,-12.038 32.589,-29.427 v -0.244 c 0,-19.699 -15.322,-29.914 -34.413,-29.914 h -14.227 v -25.536 h -18.727 z" />
+    </g>
+    <g
+       style="display:inline;fill:#ffffff;fill-opacity:1"
+       transform="matrix(0.35277777,0,0,-0.35277777,99.723703,181.66637)"
+       id="g570-4"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path568-0"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 6.202,13.376 c 2.432,-1.459 5.472,-2.553 7.904,-2.553 3.161,0 4.864,0.972 6.445,4.256 L -4.986,80.5 H 14.592 L 29.428,36.116 43.655,80.5 H 62.868 L 37.818,13.741 C 32.832,0.487 27.482,-4.499 16.416,-4.499 9.728,-4.499 4.742,-2.797 0,0" />
+    </g>
+    <g
+       style="display:inline;fill:#ffffff;fill-opacity:1"
+       transform="matrix(0.35277777,0,0,-0.35277777,125.11954,169.74057)"
+       id="g574-6"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path572-2"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 V 30.887 H -7.783 V 46.695 H 0 V 63.354 H 18.483 V 46.695 H 33.805 V 30.887 H 18.483 V 3.04 c 0,-4.256 1.824,-6.323 5.959,-6.323 3.405,0 6.445,0.851 9.12,2.31 v -14.835 c -3.891,-2.311 -8.391,-3.77 -14.592,-3.77 C 7.661,-19.578 0,-15.079 0,0" />
+    </g>
+    <g
+       style="display:inline;fill:#ffffff;fill-opacity:1"
+       transform="matrix(0.35277777,0,0,-0.35277777,153.73306,168.15332)"
+       id="g578-9"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path576-9"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 v 3.283 c -3.162,1.459 -7.296,2.432 -11.795,2.432 -7.904,0 -12.769,-3.162 -12.769,-8.998 v -0.244 c 0,-4.985 4.135,-7.904 10.093,-7.904 C -5.837,-11.431 0,-6.688 0,0 m -42.439,-4.256 v 0.243 c 0,14.228 10.822,20.794 26.266,20.794 6.566,0 11.309,-1.094 15.93,-2.675 V 15.2 c 0,7.661 -4.743,11.917 -13.984,11.917 -7.053,0 -12.039,-1.337 -17.997,-3.526 l -4.621,14.106 c 7.174,3.161 14.227,5.228 25.293,5.228 10.093,0 17.389,-2.675 22.01,-7.296 4.864,-4.864 7.053,-12.038 7.053,-20.794 V -22.983 H -0.365 v 7.053 c -4.499,-4.986 -10.701,-8.269 -19.699,-8.269 -12.282,0 -22.375,7.053 -22.375,19.943" />
+    </g>
+    <g
+       style="display:inline;fill:#ffffff;fill-opacity:1"
+       transform="matrix(0.35277777,0,0,-0.35277777,163.8145,144.94527)"
+       id="g582-0"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path580-8"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 h 18.483 v -47.303 l 21.646,23.712 H 62.26 L 37.453,-49.249 63.111,-88.769 H 41.953 l -17.025,26.63 -6.445,-6.809 V -88.769 H 0 Z" />
+    </g>
+    <g
+       style="display:inline;fill:#ffffff;fill-opacity:1"
+       transform="matrix(0.35277777,0,0,-0.35277777,186.89413,168.15332)"
+       id="g586-1"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path584-3"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 V 42.196 H 18.483 V 5.837 c 0,-8.755 4.135,-13.255 11.188,-13.255 7.053,0 11.552,4.5 11.552,13.255 V 42.196 H 59.706 V -22.983 H 41.223 v 9.242 C 36.967,-19.213 31.495,-24.199 22.131,-24.199 8.147,-24.199 0,-14.957 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,53.298393,132.49581)"
+       id="g590-1"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path588-1"
+         style="fill:#ef3d26;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 h -130.308 c -8.959,0 -16.289,-7.33 -16.289,-16.288 v -35.809 -23.906 -70.594 c 0,-8.959 7.33,-16.288 16.289,-16.288 h 30.653 l -14.241,26.358 h -0.124 v 0.229 84.201 25.738 h 52.72 c 18,0 33.649,-10.53 40.827,-25.854 l 36.762,-68.039 V -16.288 C 16.289,-7.33 8.959,0 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,59.044643,184.21194)"
+       id="g594-0"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path592-3"
+         style="fill:#b22a24;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 v 26.345 l -36.762,68.039 c 2.677,-5.714 4.185,-12.091 4.185,-18.843 0,-24.4 -19.985,-44.544 -44.538,-44.544 V 10.07 h -53.07 l 14.242,-26.359 h 99.654 C -7.33,-16.289 0,-8.959 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,46.075973,150.91526)"
+       id="g598-4"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path596-0"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c -7.178,15.324 -22.827,25.854 -40.827,25.854 h -52.72 V -84.085 l 0.124,-0.229 h 53.07 v 20.926 c 24.553,0 44.537,20.145 44.537,44.544 C 4.184,-12.091 2.677,-5.715 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,20.923133,151.25996)"
+       id="g602-3"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path600-9"
+         style="fill:#bf3326;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 13.489,-6.405 c 1.521,-0.72 2.442,-1.841 2.442,-3.402 v -0.4 c 0,-1.561 -0.921,-2.682 -2.442,-3.403 L 0,-20.014 c -0.48,-0.24 -0.921,-0.4 -1.481,-0.4 -1.281,-0.04 -2.442,1.081 -2.442,2.521 0,1.161 0.641,1.962 1.681,2.442 l 12.089,5.404 -12.089,5.404 c -1.04,0.48 -1.681,1.401 -1.681,2.522 0,1.481 1.161,2.521 2.642,2.481 C -0.841,0.36 -0.4,0.2 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,39.703783,158.32042)"
+       id="g606-1"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path604-9"
+         style="fill:#bf3326;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 -13.489,6.404 c -1.521,0.721 -2.442,1.842 -2.442,3.403 v 0.4 c 0,1.561 0.921,2.682 2.442,3.402 L 0,20.014 c 0.48,0.24 0.921,0.4 1.481,0.4 1.281,0.04 2.442,-1.081 2.442,-2.521 0,-1.161 -0.641,-1.962 -1.681,-2.442 L -9.847,10.047 2.242,4.643 C 3.282,4.163 3.923,3.242 3.923,2.121 3.923,0.64 2.762,-0.4 1.281,-0.36 0.841,-0.36 0.4,-0.2 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,34.683333,159.38951)"
+       id="g610-6"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path608-9"
+         style="fill:#bf3326;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c -1.393,0 -2.543,-1.15 -2.543,-2.543 h 0.003 c 0,-3.042 -1.521,-4.844 -3.483,-4.844 h -0.08 c -2.161,0 -3.442,2.042 -3.442,4.844 h 0.004 c 0,1.558 -1.285,2.844 -2.844,2.844 -1.558,0 -2.844,-1.286 -2.844,-2.844 0,-2.802 -1.281,-4.844 -3.442,-4.844 h -0.08 c -1.962,0 -3.483,1.802 -3.483,4.844 h 0.003 c 0,1.393 -1.15,2.543 -2.543,2.543 -1.394,0 -2.544,-1.15 -2.544,-2.543 0,-6.205 3.043,-10.808 8.246,-10.808 h 0.08 c 3.323,0 5.124,1.681 6.605,4.523 1.481,-2.842 3.282,-4.523 6.605,-4.523 h 0.08 c 5.203,0 8.245,4.603 8.245,10.808 C 2.543,-1.15 1.394,0 0,0" />
+    </g>
+    <g
+       style="display:inline"
+       transform="matrix(0.35277777,0,0,-0.35277777,13.074703,180.65936)"
+       id="g614-3"
+       inkscape:export-xdpi="733.85999"
+       inkscape:export-ydpi="733.85999">
+      <path
+         inkscape:connector-curvature="0"
+         id="path612-3"
+         style="fill:#424143;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 H 0.124 L 0,0.229 Z" />
+    </g>
+  </g>
+</svg>
diff --git a/src/pytaku/templates/base.html b/src/pytaku/templates/base.html
new file mode 100644
index 0000000..77dbb0c
--- /dev/null
+++ b/src/pytaku/templates/base.html
@@ -0,0 +1,55 @@
+{# vim: ft=htmldjango
+#}
+{% macro ibutton(icon, text, color='red') -%}
+<button class="{{ color }} button">
+  <img src="{{ url_for('static', filename='icons/' + icon + '.svg')}}" alt="{{ text }} icon" />
+  <span>{{ text }}</span>
+</button>
+{%- endmacro %}
+
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>
+      {% block title %}
+      {% endblock %}
+      - Pytaku
+    </title>
+
+    <link rel="stylesheet" href="{{ url_for('static', filename='minireset.css') }}" />
+    <link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}" />
+
+    {% block head %}
+    {% endblock %}
+  </head>
+
+  <body>
+    <nav>
+      <a class="logo" href="/"><img src="{{ url_for('static', filename='pytaku.svg')}}" alt="home" /></a>
+      <form class="search" action="{{ url_for('search_view') }}">
+        <input type="text" placeholder="search manga" /><button class="red button">
+          <img src="{{ url_for('static', filename='icons/search.svg')}}" alt="search">
+        </button>
+      </form>
+      <span class="links">
+        <a href="/bookmarks">
+          <img src="{{ url_for('static', filename='icons/bookmark.svg')}}" alt="bookmarks" />
+          <span>Bookmarks</span>
+        </a>
+        <a href="/account">
+          <img src="{{ url_for('static', filename='icons/user.svg')}}" alt="account" />
+          <span>Account</span>
+        </a>
+      </span>
+      </ul>
+    </nav>
+
+    <div class="content">
+    {% block content %}
+    {% endblock %}
+    </div>
+  </body>
+
+</html>
diff --git a/src/pytaku/templates/chapter.html b/src/pytaku/templates/chapter.html
new file mode 100644
index 0000000..14e7466
--- /dev/null
+++ b/src/pytaku/templates/chapter.html
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+
+{% block title %}
+Chapter {{ id }}
+{% endblock %}
+
+{% block content %}
+Hello {{ id }}
+{% endblock %}
diff --git a/src/pytaku/templates/title.html b/src/pytaku/templates/title.html
new file mode 100644
index 0000000..a6ba007
--- /dev/null
+++ b/src/pytaku/templates/title.html
@@ -0,0 +1,40 @@
+{% extends 'base.html' %}
+
+{% block head %}
+<style>
+  .cover {
+    width: 400px;
+    border: 1px solid black;
+  }
+</style>
+{% endblock %}
+
+{% block title %}
+{{ name }}
+{% endblock %}
+
+{% block content %}
+
+<h1>{{ name }}</h1>
+
+{{ ibutton('bookmark', 'Bookmark', 'blue') }}
+
+<img class="cover" src="{{ cover }}" alt="cover" />
+
+<table>
+  <tr>
+    <th>Name</th>
+    <th>Group</th>
+  </tr>
+  {% for chapter in chapters %}
+  <tr>
+    <td>
+      <a href="{{ url_for('chapter_view', chapter_id=chapter['id']) }}">
+        Ch.{{ chapter['number'] }} - {{ chapter['name'] }}
+      </a>
+    </td>
+    <td>{{ chapter['group'] }}</td>
+  </tr>
+  {% endfor %}
+</table>
+{% endblock %}